// Base URL of the Laravel JSON API (set in resources/views/app.blade.php)
const API_URL = (window.APP_CONFIG && window.APP_CONFIG.apiBase ? window.APP_CONFIG.apiBase : "api") + "/";
const { useState, useEffect, useLayoutEffect, useRef, useMemo, useCallback } = React;
const BN_DIGITS = ['০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯'];
const BN_OPTIONS = ['ক', 'খ', 'গ', 'ঘ', 'ঙ', 'চ'];
const EN_OPTIONS = ['A', 'B', 'C', 'D', 'E', 'F'];
function toBengaliNumber(val) {
if (val === null || val === undefined) return '';
return String(val).replace(/\d/g, (d) => BN_DIGITS[Number(d)]);
}
function stimulusQuestionNumbers(items, index) {
const [start, end] = questionBlockBounds(items || [], index);
const head = (items || [])[start] || {};
if (start === index && !(head.showStimulus || head.sharedStimulus || head.stimulusImage)) return [index + 1];
if (!(head.showStimulus || head.sharedStimulus || head.stimulusImage)) return [index + 1];
return Array.from({ length: Math.max(1, end - start) }, (_, offset) => start + offset + 1);
}
function stimulusInstruction(items, index) {
const numbers = stimulusQuestionNumbers(items, index);
const formatted = numbers.map(toBengaliNumber);
let questionNumbers;
if (numbers.length === 1) questionNumbers = formatted[0];
else if (numbers.length === 2) questionNumbers = `${formatted[0]} ও ${formatted[1]}`;
else if (numbers.every((number, i) => i === 0 || number === numbers[i - 1] + 1)) questionNumbers = `${formatted[0]} থেকে ${formatted[formatted.length - 1]}`;
else questionNumbers = `${formatted.slice(0, -1).join(', ')} ও ${formatted[formatted.length - 1]}`;
return `নিচের উদ্দীপকটি পড়ে ${questionNumbers} নং প্রশ্নের উত্তর দাও:`;
}
function isGenericStimulusInstruction(value) {
if (typeof value !== 'string') return false;
const text = plainText(value).replace(/\s+/g, ' ').trim();
return /নিচের উদ্দীপক(?:টি|কের|কে)?\s*(?:(?:পড়ে|এর আলোকে|আলোকে)\s*)?(?:দুইটি|দুটি|২টি) প্রশ্নের উত্তর দাও/.test(text);
}
function getStimulusInstruction(item, items, index) {
return item.stimulusInstruction !== undefined && !isGenericStimulusInstruction(item.stimulusInstruction)
? item.stimulusInstruction
: stimulusInstruction(items, index);
}
function generateId(prefix = 'id') {
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
}
const SAMPLE_DIAGRAMS = [
{ id: 'cell_diagram', name: 'উদ্ভিদ কোষ ও অঙ্গাণু (Plant Cell)', svg: `গহ্বর (Vacuole) নিউক্লিয়াস (A) ক্লোরোপ্লাস্ট (B) ` },
{ id: 'geometry_triangle', name: 'জ্যামিতিক ত্রিভুজ (Triangle ABC)', svg: `A B C D ` },
{ id: 'circle', name: 'বৃত্ত ও ব্যাসার্ধ (Circle)', svg: `O A B r ` },
{ id: 'rect', name: 'আয়ত ABCD (Rectangle)', svg: `A B C D ` },
{ id: 'right_tri', name: 'সমকোণী ত্রিভুজ (Right Triangle)', svg: `A B C ` },
{ id: 'transversal', name: 'সমান্তরাল রেখা ও ছেদক', svg: `1 2 3 4 AB CD ` },
{ id: 'numline', name: 'সংখ্যারেখা (Number Line)', svg: `-3 -2 -1 0 1 2 3 ` },
{ id: 'circuit', name: 'বৈদ্যুতিক বর্তনী (Circuit)', svg: `বা সুইচ ব্যাটারি ` },
{ id: 'magnet', name: 'দণ্ড চুম্বক (Bar Magnet)', svg: `N S ` },
{ id: 'beaker', name: 'বীকার (Beaker)', svg: `তরল ` }
];
function createSampleExamPaper() {
return {
id: generateId('paper'),
title: 'দাখিল ৭ম শ্রেণি — বিজ্ঞান',
updatedAt: new Date().toISOString(),
header: { institutionName: 'বাহিরঘরিয়া গোপালপুর আলিম মাদ্রাসা', examName: 'অর্ধবার্ষিক পরীক্ষা - ২০২৬', className: 'দাখিল সপ্তম শ্রেণি', subject: 'বিজ্ঞান', subjectCode: '১২৭', duration: '৩ ঘণ্টা', totalMarks: '১০০', alignment: 'center', instFontSize: 19, subFontSize: 13, boldHeader: true, showBorderBottom: true },
layout: { fontFamily: 'Noto Serif Bengali', fontSize: 11.5, lineHeight: 1.42, questionSpacing: 7, marginTop: 12, marginBottom: 12, marginLeft: 12, marginRight: 12, columnGap: 7, showColumnDivider: true, showPageNumber: true, footerText: 'বাহিরঘরিয়া গোপালপুর আলিম মাদ্রাসা — অর্ধবার্ষিক পরীক্ষা ২০২৬', showFooter: true, optionLabelStyle: 'bn' },
sections: {
mcq: {
enabled: true, title: 'বহুনির্বাচনী অভীক্ষা', marksText: 'মান - ৩০', instruction: 'বিশেষ দ্রষ্টব্য: প্রতিটি প্রশ্নের মান ১। সকল প্রশ্নের উত্তর দাও। সঠিক উত্তরটি উত্তরপত্রে লেখো।', columns: 2, pageBreakBefore: false,
items: [
{ id: 'mcq_1', type: 'standard', text: 'কোনটি অকোষীয় অণুজীব?', marks: 1, correctIndex: 0, layoutMode: 'auto', options: [ { id: 'o1', text: 'ভাইরাস', widthMode: 'auto' }, { id: 'o2', text: 'শৈবাল', widthMode: 'auto' }, { id: 'o3', text: 'ছত্রাক', widthMode: 'auto' }, { id: 'o4', text: 'ব্যাকটেরিয়া', widthMode: 'auto' } ] }
]
},
creative: {
enabled: true, title: 'সৃজনশীল প্রশ্ন', marksText: 'মান - ৫০', instruction: '[ডান পাশের সংখ্যা প্রশ্নের পূর্ণমান জ্ঞাপক। যেকোনো ৫টি প্রশ্নের উত্তর দাও]', columns: 1, pageBreakBefore: true,
items: [
{ id: 'cq_1', stimulus: 'পাট আমাদের দেশের সোনালি আঁশ।', subQuestions: [ { label: 'ক', text: 'ব্যাকটেরিয়া কী?', marks: 1 }, { label: 'খ', text: 'ব্যাকটেরিয়াকে আদিকোষী বলা হয় কেন?', marks: 2 }, { label: 'গ', text: 'উদ্ভিদ ও প্রাণীর রোগের তালিকা তৈরি কর।', marks: 3 }, { label: 'ঘ', text: 'অর্থনৈতিক গুরুত্ব বিশ্লেষণ কর।', marks: 4 } ] }
]
},
short: {
enabled: true, title: 'সংক্ষিপ্ত প্রশ্ন', marksText: 'মান - ২০', instruction: '[সকল প্রশ্নের মান ২। যেকোনো ১০টি প্রশ্নের উত্তর দাও]', columns: 2, pageBreakBefore: false,
items: [
{ id: 'sq_1', text: 'কাজ ও ক্ষমতার মধ্যে পার্থক্য লেখ।', marks: 2 }
]
}
},
labels: { subjectCode: 'বিষয় কোড', mcqMarksText: 'মান - ৩০', creativeMarksText: 'মান - ৫০', shortMarksText: 'মান - ২০', mcqInstruction: 'সকল প্রশ্নের উত্তর দাও।', creativeInstruction: '[যেকোনো ৫টি প্রশ্নের উত্তর দাও]', shortInstruction: '[যেকোনো ১০টি প্রশ্নের উত্তর দাও]' }
};
}
const FIXED_SECTION_ORDER = ['mcq', 'creative', 'short'];
const SECTION_TITLES = { mcq: 'বহুনির্বাচনী অভীক্ষা', creative: 'সৃজনশীল প্রশ্ন', short: 'সংক্ষিপ্ত প্রশ্ন' };
const sectionOrderFor = (paper) => {
const keys = Object.keys(paper?.sections || {});
const saved = (paper?.sectionOrder || []).filter(k => keys.includes(k));
return [...saved, ...FIXED_SECTION_ORDER.filter(k => keys.includes(k) && !saved.includes(k)), ...keys.filter(k => !saved.includes(k) && !FIXED_SECTION_ORDER.includes(k))];
};
const ConfirmCtx = React.createContext(() => {});
function nextPaperTitle(papers) {
const max = (papers || []).reduce((m, p) => { const r = /^New - (\d+)/.exec(p.title || ''); return r ? Math.max(m, Number(r[1])) : m; }, 0);
return `New - ${max + 1} শ্রেণি — Untitled_Subject`;
}
const plainText = (html) => String(html || '').replace(/<[^>]*>/g, '').trim();
// Section marks: label override -> section config -> computed from the questions
function secMarks(paper, k) {
const manual = paper?.labels?.[`${k}MarksText`] || paper?.sections?.[k]?.marksText;
if (manual) return manual;
const items = paper?.sections?.[k]?.items || [];
const total = items.reduce((s, it) => s + (k === 'creative' ? (it.subQuestions || []).reduce((a, q) => a + (Number(q.marks) || 0), 0) : (Number(it.marks) || (k === 'mcq' ? 1 : 2))), 0);
return `মান - ${toBengaliNumber(total)}`;
}
function ConfirmDialog({ dlg, onClose }) {
useEffect(() => {
if (!dlg) return;
const h = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h);
}, [dlg]);
if (!dlg) return null;
return (
{ e.preventDefault(); execCmd('bold'); }} className="px-1.5 py-0.5 rounded hover:bg-slate-200 font-bold">B
{ e.preventDefault(); execCmd('italic'); }} className="px-1.5 py-0.5 rounded hover:bg-slate-200 italic">I
{ e.preventDefault(); execCmd('underline'); }} className="px-1.5 py-0.5 rounded hover:bg-slate-200 underline">U
{ e.preventDefault(); execCmd('subscript'); }} className="px-1.5 py-0.5 rounded hover:bg-slate-200">X₂
{ e.preventDefault(); execCmd('superscript'); }} className="px-1.5 py-0.5 rounded hover:bg-slate-200">X²
setShowSymbols(!showSymbols)} className={`px-2 py-0.5 rounded text-[11px] font-medium transition ${showSymbols ? 'bg-emerald-100 text-emerald-800' : 'hover:bg-slate-200 text-slate-600'}`}>Ω সংকেত
{showSymbols && (
{SYMBOLS.map((s) => ( { e.preventDefault(); insertSymbol(s); }} className="px-1.5 py-0.5 text-xs bg-white border border-slate-200 rounded hover:border-emerald-500 font-medium">{s} ))}
)}
onChange(e.currentTarget.innerHTML)} style={{ minHeight }} className={`rich-editable px-2.5 ${compact ? 'py-1.5 text-sm' : 'py-2 text-[14.5px]'} leading-relaxed font-serifBn`} />
);
}
function loadSavedImages() { try { return JSON.parse(localStorage.getItem('qm_images') || '[]'); } catch (e) { return []; } }
function saveImages(list) { try { localStorage.setItem('qm_images', JSON.stringify(list)); return true; } catch (e) { return false; } }
function shrinkImage(dataUrl, maxPx = 900) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const r = Math.min(1, maxPx / Math.max(img.width, img.height));
const c = document.createElement('canvas'); c.width = Math.round(img.width * r); c.height = Math.round(img.height * r);
const ctx = c.getContext('2d'); ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, c.width, c.height); ctx.drawImage(img, 0, 0, c.width, c.height);
resolve(c.toDataURL('image/jpeg', 0.85));
};
img.onerror = () => resolve(dataUrl);
img.src = dataUrl;
});
}
const imgHtml = (src) => (src && src.startsWith('
`;
function DiagramInserter({ currentImage, onSelectImage }) {
const [open, setOpen] = useState(false);
const [saved, setSaved] = useState(loadSavedImages);
const [msg, setMsg] = useState('');
const fileInputRef = useRef(null);
const askConfirm = React.useContext(ConfirmCtx);
useEffect(() => {
if (!open) return;
const h = (e) => { if (e.key === 'Escape') setOpen(false); };
window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h);
}, [open]);
const handleFileUpload = (e) => {
const files = Array.from(e.target.files || []);
e.target.value = '';
files.forEach((file) => {
const reader = new FileReader();
reader.onload = async (ev) => {
const small = file.type === 'image/svg+xml' ? ev.target.result : await shrinkImage(ev.target.result);
const entry = { id: generateId('img'), name: file.name.replace(/\.[^.]+$/, '').slice(0, 30), src: small };
setSaved((prev) => { const next = [entry, ...prev]; if (!saveImages(next)) setMsg('ব্রাউজারে লাইব্রেরি সেভ হয়নি (জায়গা কম), তবে ছবিটি ব্যবহার করা যাবে।'); return next; });
if (files.length === 1) { onSelectImage(small); setOpen(false); }
};
reader.readAsDataURL(file);
});
};
const deleteSaved = (img) => askConfirm({
title: 'আপলোড করা ছবি মুছবেন?', message: `"${img.name}" আপনার ছবির তালিকা থেকে মুছে যাবে। (যেসব প্রশ্নে ব্যবহার হয়েছে সেখানে থাকবে)`,
onConfirm: () => setSaved((prev) => { const next = prev.filter((x) => x.id !== img.id); saveImages(next); return next; })
});
const removeCurrent = () => askConfirm({ title: 'চিত্র মুছবেন?', message: 'এই প্রশ্ন থেকে চিত্রটি সরিয়ে ফেলা হবে।', onConfirm: () => onSelectImage(null) });
const Card = ({ src, name, onPick, onDelete }) => (
);
return (
setOpen(true)} className="px-2.5 py-1 text-xs font-medium rounded-md border bg-white hover:bg-slate-50 flex items-center gap-1">🖼️ {currentImage ? 'চিত্র পরিবর্তন' : 'চিত্র যোগ করুন'}
{currentImage && (
<>
🗑 চিত্র মুছুন
>
)}
{open && (
setOpen(false)}>
e.stopPropagation()} className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[88vh] flex flex-col">
চিত্র নির্বাচন করুন
setOpen(false)} className="w-8 h-8 rounded-full hover:bg-slate-100 text-slate-500">✕
{msg &&
{msg}
}
{saved.length === 0 ?
এখনো কোনো ছবি আপলোড করা হয়নি। আপলোড করলে এখানে জমা থাকবে এবং পরে আবার ব্যবহার করা যাবে।
: (
{saved.map((img) => { onSelectImage(img.src); setOpen(false); }} onDelete={() => deleteSaved(img)} />)}
)}
রেডিমেড চিত্র
{SAMPLE_DIAGRAMS.map((d) => { onSelectImage(d.svg); setOpen(false); }} />)}
)}
);
}
function StatementEditor({ statements, layoutMode = 'auto', onChange, onLayoutChange }) {
const askConfirm = React.useContext(ConfirmCtx);
const ROMAN = ['i', 'ii', 'iii', 'iv', 'v', 'vi'];
const set = (i, v) => onChange(statements.map((x, j) => j === i ? v : x));
const move = (i, d) => { const t = i + d; if (t < 0 || t >= statements.length) return; const a = [...statements]; [a[i], a[t]] = [a[t], a[i]]; onChange(a); };
return (
বিবৃতিসমূহ ({ROMAN.slice(0, statements.length).join(', ')})
{onLayoutChange && (
onLayoutChange('auto')} className={`px-2 py-0.5 rounded ${layoutMode==='auto'?'bg-emerald-700 text-white font-bold':'text-slate-600'}`}>অটো
onLayoutChange('3col')} className={`px-2 py-0.5 rounded ${layoutMode==='3col'?'bg-emerald-700 text-white font-bold':'text-slate-600'}`}>৩ কলাম
onLayoutChange('1col')} className={`px-2 py-0.5 rounded ${layoutMode==='1col'?'bg-emerald-700 text-white font-bold':'text-slate-600'}`}>১ কলাম
)}
= 6} onClick={() => onChange([...statements, ''])} className="px-2.5 py-1 text-xs font-bold rounded-lg bg-slate-100 hover:bg-slate-200 disabled:opacity-40">+ বিবৃতি যোগ
{statements.map((st, i) => (
{ROMAN[i]}
set(i, v)} placeholder={`বিবৃতি ${ROMAN[i]} লিখুন...`} compact lite minHeight="30px" />
move(i, -1)} disabled={i === 0} className="px-1.5 py-1 text-[11px] rounded border bg-white hover:bg-slate-100 disabled:opacity-30">↑
move(i, 1)} disabled={i === statements.length - 1} className="px-1.5 py-1 text-[11px] rounded border bg-white hover:bg-slate-100 disabled:opacity-30">↓
askConfirm({ title: 'বিবৃতি মুছবেন?', message: `বিবৃতি ${ROMAN[i]} মুছে ফেলা হবে।`, onConfirm: () => onChange(statements.filter((_, j) => j !== i)) })} className="px-1.5 py-1 text-[11px] rounded border border-rose-200 text-rose-600 bg-white hover:bg-rose-50">✕
))}
{statements.length === 0 &&
কোনো বিবৃতি নেই
}
);
}
function MCQOptionLayoutEditor({ question, optionLabelStyle, onChangeQuestion }) {
const labels = optionLabelStyle === 'en' ? EN_OPTIONS : BN_OPTIONS;
const [expandedOpt, setExpandedOpt] = useState(null);
const updateOption = (idx, patch) => {
const nextOpts = question.options.map((o, i) => i === idx ? { ...o, ...patch } : o);
onChangeQuestion({ ...question, options: nextOpts });
};
const moveOption = (idx, dir) => {
const target = idx + dir;
if(target < 0 || target >= question.options.length) return;
const nextOpts = [...question.options];
[nextOpts[idx], nextOpts[target]] = [nextOpts[target], nextOpts[idx]];
onChangeQuestion({ ...question, options: nextOpts });
if(expandedOpt === idx) setExpandedOpt(target);
else if(expandedOpt === target) setExpandedOpt(idx);
};
const applyPresetLayout = (mode) => {
let nextOpts = [...question.options];
if (mode === '4col') nextOpts = nextOpts.map((o) => ({ ...o, widthMode: '25%', breakBefore: false }));
else if (mode === '2col') nextOpts = nextOpts.map((o) => ({ ...o, widthMode: '50%', breakBefore: false }));
else if (mode === '1col') nextOpts = nextOpts.map((o) => ({ ...o, widthMode: '100%', breakBefore: false }));
else if (mode === 'auto') nextOpts = nextOpts.map((o) => ({ ...o, widthMode: 'auto', breakBefore: false }));
onChangeQuestion({ ...question, layoutMode: mode, options: nextOpts });
};
return (
অপশন লেআউট ও ফরম্যাটিং
{[{ id: 'auto', label: 'অটো' }, { id: '4col', label: '৪ কলাম' }, { id: '2col', label: '২ কলাম' }, { id: '1col', label: '১ কলাম' }, { id: 'custom', label: 'কাস্টম' }].map((m) => (
applyPresetLayout(m.id)} className={`px-2.5 py-1 rounded-md font-medium ${ (question.layoutMode || 'auto') === m.id ? 'bg-emerald-700 text-white' : 'text-slate-600 hover:bg-white' }`}>{m.label}
))}
{question.options.map((opt, idx) => {
const isCorrect = question.correctIndex === idx;
const isExp = expandedOpt === idx;
return (
onChangeQuestion({ ...question, correctIndex: idx })} className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold shrink-0 border mt-1.5 ${isCorrect ? 'bg-emerald-600 text-white border-emerald-600' : 'bg-white text-slate-600 border-slate-300 hover:border-emerald-500'}`}>{labels[idx]}
{/* এখানে সাধারণ ইনপুটের বদলে RichTextField ব্যবহার করা হলো */}
updateOption(idx, { text: val })}
compact={true}
lite={true}
minHeight="34px"
placeholder={`অপশন ${labels[idx]}...`}
/>
{ onChangeQuestion({ ...question, layoutMode: 'custom', options: question.options.map((o, i) => i === idx ? { ...o, widthMode: e.target.value } : o ) }); }} className="text-[11px] border rounded px-1.5 py-1 outline-none">
Auto ¼ (25%) ½ (50%) পূর্ণ (100%)
{ onChangeQuestion({ ...question, layoutMode: 'custom', options: question.options.map((o, i) => i === idx ? { ...o, breakBefore: !o.breakBefore } : o ) }); }} className={`px-1.5 py-1 rounded text-[11px] border ${opt.breakBefore ? 'bg-amber-100 border-amber-400 font-semibold' : 'bg-white'}`}>↵ লাইন
setExpandedOpt(isExp ? null : idx)} className={`px-1.5 py-1 rounded text-[11px] border ${isExp ? 'bg-slate-700 text-white' : 'bg-white text-slate-600 hover:bg-slate-100'}`} title="কাস্টমাইজ">⚙️
moveOption(idx, -1)} disabled={idx===0} className="px-1.5 py-1 rounded text-[11px] border bg-white text-slate-600 hover:bg-slate-100 disabled:opacity-40">↑
moveOption(idx, 1)} disabled={idx===question.options.length-1} className="px-1.5 py-1 rounded text-[11px] border bg-white text-slate-600 hover:bg-slate-100 disabled:opacity-40">↓
{isExp && (
অ্যালাইনমেন্ট:
updateOption(idx, {align: e.target.value})} className="border border-slate-300 rounded px-2 py-1 bg-white outline-none">
বামে (Left) মাঝে (Center) ডানে (Right)
ফন্ট সাইজ:
updateOption(idx, {fontSizeOffset: e.target.value})} className="border border-slate-300 rounded px-2 py-1 bg-white outline-none">
ছোট (-1.5pt) সামান্য ছোট (-0.5pt) স্বাভাবিক (Default) বড় (+1pt)
)}
);
})}
);
}
function MCQOptionGrid({ options, layoutMode = 'auto', optionLabelStyle = 'bn', showAnswerKey = false, correctIndex = 0, fontSizePt = 11.5, compact = false }) {
const labels = optionLabelStyle === 'en' ? EN_OPTIONS : BN_OPTIONS;
const computedOptions = useMemo(() => {
if (!options) return [];
if (layoutMode === '1col') return options.map(opt => ({ ...opt, resolvedWidth: '100%', resolvedBreak: false }));
if (layoutMode === '2col') return options.map(opt => ({ ...opt, resolvedWidth: '50%', resolvedBreak: false }));
if (layoutMode === '4col') return options.map(opt => ({ ...opt, resolvedWidth: '25%', resolvedBreak: false }));
if (layoutMode === 'custom') return options.map(opt => ({ ...opt, resolvedWidth: opt.widthMode === 'custom' ? `${opt.customWidth || 50}%` : opt.widthMode, resolvedBreak: Boolean(opt.breakBefore) }));
const plainLengths = options.map(o => (o.text || '').replace(/<[^>]*>/g, '').trim().length);
const maxLen = Math.max(...plainLengths);
if (maxLen > 34) return options.map(opt => ({ ...opt, resolvedWidth: '100%', resolvedBreak: false }));
if (maxLen <= 10) return options.map(opt => ({ ...opt, resolvedWidth: '25%', resolvedBreak: false }));
return options.map((opt, idx) => ({ ...opt, resolvedWidth: plainLengths[idx] > 22 ? '100%' : '50%', resolvedBreak: false }));
}, [options, layoutMode]);
return (
{computedOptions.map((opt, idx) => (
{opt.resolvedBreak && idx > 0 &&
}
{labels[idx] || idx + 1}.
))}
);
}
function QuestionPaperHeader({ header, labels }) {
if(!header) return null;
const alignClass = header.alignment === 'left' ? 'text-left' : header.alignment === 'right' ? 'text-right' : 'text-center';
const hasSubjectCode = header.subjectCode && header.subjectCode.trim() !== '';
return (
{header.institutionName}
{header.examName &&
{header.examName}
}
{header.className &&
{header.className}
}
{hasSubjectCode ? (
{header.subject &&
বিষয়: {header.subject}
}
{header.duration &&
সময়: {header.duration}
}
{labels?.subjectCode || 'বিষয় কোড'}: {toBengaliNumber(header.subjectCode)}
{header.totalMarks &&
পূর্ণমান: {toBengaliNumber(header.totalMarks)}
}
) : (
{header.subject &&
বিষয়: {header.subject}
}
{header.duration &&
সময়: {header.duration}
}
{header.totalMarks &&
পূর্ণমান: {toBengaliNumber(header.totalMarks)}
}
)}
{header.showBorderBottom &&
}
);
}
function SectionHeaderBlock({ section, secKey, paper }) {
if (!section) return null;
const marksText = section.marksText !== undefined && section.marksText !== '' ? section.marksText : secMarks(paper, secKey);
const instruction = section.instruction;
const title = section.title || (secKey === 'mcq' ? 'বহুনির্বাচনী অভীক্ষা' : secKey === 'creative' ? 'সৃজনশীল প্রশ্ন' : 'সংক্ষিপ্ত প্রশ্ন');
return (
{title}
{marksText && {marksText} }
{instruction &&
{instruction}
}
);
}
function MCQItemView({ item, index, layout, showAnswerKey, items }) {
const roman = ['i', 'ii', 'iii', 'iv', 'v', 'vi'];
let statementClass = "my-1 pl-2 text-[0.97em] ";
if (item.statementLayoutMode === '1col') statementClass += "flex flex-col space-y-0.5";
else if (item.statementLayoutMode === '3col') statementClass += "grid grid-cols-3 gap-1";
else statementClass += "flex flex-wrap gap-x-5 gap-y-1";
const defaultInst = item.showStimulus || item.sharedStimulus || item.stimulusImage
? getStimulusInstruction(item, items || [], index)
: `নিচের উদ্দীপকের আলোকে ${toBengaliNumber(index + 1)} নং প্রশ্নের উত্তর দাও:`;
const instText = defaultInst;
return (
{((item.sharedStimulus && item.sharedStimulus.trim() !== '') || item.stimulusImage) && (
{instText}
{item.sharedStimulus &&
}
{item.stimulusImage &&
` }} />}
)}
{toBengaliNumber(index + 1)}.
{item.image &&
` }} />}
{item.type === 'statement' && (
<>
{(item.statements||[]).map((st, sIdx) => (
{roman[sIdx] || sIdx + 1}.
))}
{item.subPrompt &&
{item.subPrompt}
}
>
)}
);
}
function CreativeItemView({ item, index, layout }) {
return (
{toBengaliNumber(index + 1)}.
{item.image &&
` }} />}
{(item.subQuestions || []).map((sq, idx) => (
{sq.label}.
{toBengaliNumber(sq.marks)}
))}
);
}
function ShortItemView({ item, index, layout }) {
return (
{toBengaliNumber(index + 1)}.
{item.showIndividualMarks &&
[{toBengaliNumber(item.marks)}] }
);
}
// --- PERFECT COLUMN BALANCING ENGINE ---
function partitionItems(items, numCols, capacity, spacing) {
if (numCols === 1) {
let sum = items.reduce((a, b) => a + b.height + spacing, 0) - spacing;
if (sum <= capacity) return { cols: [items], maxHeight: sum };
return null;
}
let bestDiff = Infinity;
let validPart = null;
for (let split = 0; split <= items.length; split++) {
let left = items.slice(0, split);
let right = items.slice(split);
let hasInvalidManualBreak = false;
if (left.findIndex((item, i) => i > 0 && item.item.columnBreakBefore) !== -1) hasInvalidManualBreak = true;
if (right.findIndex((item, i) => i > 0 && item.item.columnBreakBefore) !== -1) hasInvalidManualBreak = true;
if (hasInvalidManualBreak) continue;
let leftSum = left.reduce((a, b) => a + b.height + spacing, 0);
let rightSum = right.reduce((a, b) => a + b.height + spacing, 0);
if (left.length > 0) leftSum -= spacing;
if (right.length > 0) rightSum -= spacing;
if (leftSum <= capacity && rightSum <= capacity) {
let diff = Math.abs(leftSum - rightSum);
if (diff < bestDiff || (diff === bestDiff && left.length >= right.length)) {
bestDiff = diff;
validPart = { cols: [left, right], maxHeight: Math.max(leftSum, rightSum) };
}
}
}
return validPart;
}
function PrintDocument({ paper, showAnswerKey = false, zoom = 1, containerId = '' }) {
const { header, layout, sections, labels } = paper;
const measureRootRef = useRef(null);
const [pages, setPages] = useState([]);
const [fontTick, setFontTick] = useState(0);
useEffect(() => {
// Re-paginate whenever the hidden measuring copy changes size (web fonts finishing loading, images, etc.)
let raf = 0;
const bump = () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(() => setFontTick(t => t + 1)); };
const ro = typeof ResizeObserver !== 'undefined' && measureRootRef.current ? new ResizeObserver(bump) : null;
if (ro) ro.observe(measureRootRef.current);
document.fonts?.addEventListener?.('loadingdone', bump);
document.fonts?.ready?.then(bump);
return () => { cancelAnimationFrame(raf); ro && ro.disconnect(); document.fonts?.removeEventListener?.('loadingdone', bump); };
}, []);
const MM_TO_PX = 3.7795275591;
const fontFamilyCss = layout?.fontFamily === 'Noto Sans Bengali' ? '"Noto Sans Bengali", sans-serif' : layout?.fontFamily === 'Hind Siliguri' ? '"Hind Siliguri", sans-serif' : layout?.fontFamily === 'Tiro Bangla' ? '"Tiro Bangla", serif' : '"Noto Serif Bengali", serif';
const activeSections = useMemo(() => sectionOrderFor(paper).map(k => (sections && sections[k]?.enabled) ? { secKey: k, sec: sections[k] } : null).filter(Boolean), [sections]);
const marginTop = layout?.marginTop || 12;
const marginBottom = layout?.marginBottom || 12;
const marginLeft = layout?.marginLeft || 12;
const marginRight = layout?.marginRight || 12;
const contentWidthPx = (210 - marginLeft - marginRight) * MM_TO_PX;
// Extra 10px subtracted from usable page height to prevent bottom cutoff during html2canvas render
const usablePageHeightPx = (297 - marginTop - marginBottom) * MM_TO_PX - 34;
useLayoutEffect(() => {
const rootEl = measureRootRef.current;
if (!rootEl) return;
const headerH = (rootEl.querySelector('[data-measure="exam-header"]')?.offsetHeight || 0) + 8;
const builtPages = [];
let currentPage = { pageIndex: 0, hasExamHeader: true, blocks: [] };
let remainingY = usablePageHeightPx - headerH;
const startNewPage = () => {
builtPages.push(currentPage);
currentPage = { pageIndex: builtPages.length, hasExamHeader: false, blocks: [] };
remainingY = usablePageHeightPx;
};
activeSections.forEach(({ secKey, sec }, sIdx) => {
if (sec.pageBreakBefore && sIdx > 0 && currentPage.blocks.length > 0) startNewPage();
const secHeaderH = (rootEl.querySelector(`[data-measure="sec-header-${secKey}"]`)?.offsetHeight || 34) + 8;
const numCols = Number(sec.columns) || 1;
const measuredItems = (sec.items||[]).map((item, idx) => ({ item, idx, height: rootEl.querySelector(`[data-measure="item-${secKey}-${item.id}"]`)?.offsetHeight || 48 }));
if(measuredItems.length === 0) return;
let itemCursor = 0;
let isFirstChunk = true;
while (itemCursor < measuredItems.length) {
let chunkHeaderH = isFirstChunk ? secHeaderH : 4;
if (remainingY < chunkHeaderH + measuredItems[itemCursor].height) {
startNewPage();
chunkHeaderH = isFirstChunk ? secHeaderH : 4;
}
let colCapacity = Math.max(40, remainingY - chunkHeaderH);
let candidateItems = [];
for (let i = itemCursor; i < measuredItems.length; i++) {
if (i > itemCursor && measuredItems[i].item.pageBreakBefore) break;
candidateItems.push(measuredItems[i]);
}
let bestPartition = null;
let bestFitCount = 0;
for (let count = 1; count <= candidateItems.length; count++) {
let slice = candidateItems.slice(0, count);
let partition = partitionItems(slice, numCols, colCapacity, layout?.questionSpacing || 7);
if (partition) {
bestPartition = partition;
bestFitCount = count;
} else {
break;
}
}
if (bestFitCount === 0) {
bestPartition = {
cols: Array.from({length: numCols}, (_, i) => i === 0 ? [candidateItems[0]] : []),
maxHeight: candidateItems[0].height
};
bestFitCount = 1;
}
currentPage.blocks.push({ secKey, sec, showSecHeader: isFirstChunk, columns: numCols, colItems: bestPartition.cols });
remainingY -= (chunkHeaderH + bestPartition.maxHeight + 8);
isFirstChunk = false;
itemCursor += bestFitCount;
if (itemCursor < measuredItems.length && remainingY < 40) {
startNewPage();
}
}
});
builtPages.push(currentPage);
setPages(builtPages);
}, [paper, showAnswerKey, activeSections, fontTick]);
return (
{activeSections.map(({ secKey, sec }) => (
{(sec.items||[]).map(item => (
{secKey === 'mcq' && }
{secKey === 'creative' && }
{(secKey === 'short' || !FIXED_SECTION_ORDER.includes(secKey)) && }
))}
))}
{pages.map((pg, pIdx) => (
{pg.hasExamHeader &&
}
{pg.blocks.map((blk, bIdx) => (
0 ? 'mt-2' : ''}>
{blk.showSecHeader &&
}
{blk.columns === 2 ? (
{layout?.showColumnDivider &&
}
{blk.colItems[0].map(({ item, idx }) => {blk.secKey === 'mcq' && }{blk.secKey === 'creative' && }{(blk.secKey === 'short' || !FIXED_SECTION_ORDER.includes(blk.secKey)) && } )}
{blk.colItems[1].map(({ item, idx }) => {blk.secKey === 'mcq' && }{blk.secKey === 'creative' && }{(blk.secKey === 'short' || !FIXED_SECTION_ORDER.includes(blk.secKey)) && } )}
) : (
{blk.colItems[0].map(({ item, idx }) => {blk.secKey === 'mcq' && }{blk.secKey === 'creative' && }{(blk.secKey === 'short' || !FIXED_SECTION_ORDER.includes(blk.secKey)) && } )}
)}
))}
{(layout?.showFooter !== false && (layout?.footerText || layout?.showPageNumber)) && (
{layout?.footerText}
{!pg.hasExamHeader && pg.pageIndex < pages.length -1 && [চলমান পাতা...] }{layout?.showPageNumber && পৃষ্ঠা — {toBengaliNumber(pIdx + 1)} / {toBengaliNumber(pages.length)} }
)}
))}
);
}
// --- NEW SETTINGS & USER CONTROL VIEW ---
function SettingsView({ auth, loadUsersFromParent }) {
const [oldPass, setOldPass] = useState('');
const [newPass, setNewPass] = useState('');
const [newUser, setNewUser] = useState({ username: '', password: '', role: 'user' });
const [users, setUsers] = useState([]);
const [userSearch, setUserSearch] = useState('');
const [userPage, setUserPage] = useState(1);
const [userPagination, setUserPagination] = useState({ current_page: 1, last_page: 1, total: 0 });
const [scheduleDates, setScheduleDates] = useState({});
const [activityUser, setActivityUser] = useState(null);
const [activity, setActivity] = useState({ data: [], current_page: 1, last_page: 1, total: 0 });
const askConfirm = React.useContext(ConfirmCtx);
const loadUsers = (targetPage = userPage, targetSearch = userSearch) => {
if (auth.role === 'superadmin') {
const params = new URLSearchParams({ page: targetPage, q: targetSearch });
fetch(API_URL + 'getUsers?' + params.toString()).then(res=>res.json()).then(data => {
if(data && Array.isArray(data.data)) { setUsers(data.data); setUserPagination(data); }
});
}
};
useEffect(() => { loadUsers(); }, [auth, userPage, userSearch]);
useEffect(() => {
if (!activityUser) return;
fetch(`${API_URL}userActivity?userId=${activityUser.id}&page=${activity.current_page || 1}`).then(r => r.json()).then(data => {
if (Array.isArray(data.data)) setActivity(data);
});
}, [activityUser, activity.current_page]);
const handleChangePass = (e) => {
e.preventDefault();
fetch(API_URL + 'changePassword', {method: 'POST', body: JSON.stringify({oldPassword: oldPass, newPassword: newPass})})
.then(res=>res.json()).then(data => { alert(data.message); if(data.status==='success'){setOldPass(''); setNewPass('');} });
};
const handleAddUser = (e) => {
e.preventDefault();
fetch(API_URL + 'addUser', {method: 'POST', body: JSON.stringify({newUsername: newUser.username, newPassword: newUser.password, role: newUser.role})})
.then(res=>res.json()).then(data => {
alert(data.message);
if(data.status==='success'){
setNewUser({username:'', password:'', role:'user'});
loadUsers();
}
});
};
const handleDeleteUser = (id, name) => askConfirm({ title: 'ইউজার মুছবেন?', message: `ইউজার "${name}" স্থায়ীভাবে মুছে ফেলা হবে।`, onConfirm: () => doDeleteUser(id) });
const setBankPermission = async (user, permission, allowed) => {
const res = await fetch(API_URL + 'questionBankPermission', { method: 'POST', body: JSON.stringify({ userId: user.id, permission, allowed }) }).then(r => r.json()).catch(() => ({ status: 'error' }));
if (res.status === 'success') loadUsers(); else alert(res.message || 'অনুমতি আপডেট করা যায়নি');
};
const updateAccount = async (user, action, disableAt = '') => {
const res = await fetch(API_URL + 'userAccountStatus', { method: 'POST', body: JSON.stringify({ userId: user.id, action, disable_at: disableAt }) }).then(r => r.json()).catch(() => ({ status: 'error' }));
if (res.status === 'success') loadUsers(); else alert(res.message || 'ইউজার status আপডেট করা যায়নি');
};
const eventLabel = (event) => ({ created: 'ইউজার যোগ', enabled: 'অ্যাকাউন্ট চালু', disabled: 'অ্যাকাউন্ট বন্ধ', disable_scheduled: 'ডিসেবল তারিখ নির্ধারণ', schedule_cleared: 'ডিসেবল তারিখ বাতিল', auto_disabled: 'নির্ধারিত সময়ে স্বয়ংক্রিয়ভাবে বন্ধ' }[event] || event);
const doDeleteUser = (id) => {
{
fetch(API_URL + 'deleteUser', {method: 'POST', body: JSON.stringify({userId: id})})
.then(res=>res.json()).then(data => {
alert(data.message);
loadUsers();
});
}
};
return (
সেটিংস ও কন্ট্রোল প্যানেল
🔑 আপনার পাসওয়ার্ড পরিবর্তন করুন
{auth.role === 'superadmin' && (
🛡️ অ্যাডমিন কন্ট্রোল: নতুন ইউজার তৈরি
সিস্টেম ইউজার তালিকা ({toBengaliNumber(userPagination.total || 0)})
{ setUserSearch(e.target.value); setUserPage(1); }} placeholder="ইউজারনেম খুঁজুন..." className="border rounded-lg px-3 py-2 text-sm w-full sm:w-64" />
ইউজারনেম
রোল (Role)
অ্যাকাউন্ট
Question Bank permissions
তৈরির তারিখ
অ্যাকশন / Activity
{users.map(u => (
{u.username}
{u.role.toUpperCase()}
{u.is_active ? '● Active' : '● Disabled'}
{u.disable_at && বন্ধ হবে: {new Date(u.disable_at).toLocaleString()}
}
{u.username !== 'admin' &&
updateAccount(u, u.is_active ? 'disable' : 'enable')} className={`px-2 py-1 rounded font-bold ${u.is_active ? 'bg-rose-50 text-rose-700 border border-rose-200' : 'bg-emerald-50 text-emerald-700 border border-emerald-200'}`}>{u.is_active ? 'এখনই Disable' : 'Enable'}
setScheduleDates(s => ({ ...s, [u.id]: e.target.value }))} className="border rounded px-1 py-1 text-[10px]" title="এই তারিখের শেষে অ্যাকাউন্ট বন্ধ হবে" />
updateAccount(u, 'schedule', scheduleDates[u.id] || (u.disable_at ? String(u.disable_at).slice(0, 10) : ''))} disabled={!scheduleDates[u.id] && !u.disable_at} className="px-2 py-1 rounded bg-slate-100 text-slate-700 font-bold disabled:opacity-40">তারিখ সেট
{u.disable_at && updateAccount(u, 'schedule', '')} className="px-2 py-1 rounded text-slate-500 hover:bg-slate-100">তারিখ মুছুন }
}
{u.role === 'superadmin' ? পূর্ণ অনুমতি : {[['add', 'can_add_question_bank', 'প্রশ্ন যোগ'], ['edit', 'can_edit_question_bank', 'প্রশ্ন সম্পাদনা'], ['delete', 'can_delete_question_bank', 'প্রশ্ন মুছতে']].map(([permission, key, label]) => setBankPermission(u, permission, e.target.checked)} />{label} )}
}
{new Date(u.created_at).toLocaleDateString()}
{ setActivity({ data: [], current_page: 1, last_page: 1, total: 0 }); setActivityUser(u); }} className="text-blue-700 text-xs font-bold hover:underline">Activity দেখুন
{u.username !== 'admin' && handleDeleteUser(u.id, u.username)} className="text-rose-600 text-xs font-bold hover:underline">ডিলিট করুন }
))}
{users.length === 0 && (
কোনো ইউজার নেই
)}
{userPagination.last_page > 1 &&
setUserPage(p => Math.max(1, p - 1))} className="px-3 py-1.5 border rounded bg-white disabled:opacity-40">‹ আগের
পৃষ্ঠা {toBengaliNumber(userPagination.current_page)} / {toBengaliNumber(userPagination.last_page)}
= userPagination.last_page} onClick={() => setUserPage(p => Math.min(userPagination.last_page, p + 1))} className="px-3 py-1.5 border rounded bg-white disabled:opacity-40">পরের ›
}
)}
{activityUser &&
setActivityUser(null)}>
e.stopPropagation()}>
{activityUser.username} — Activity history ইউজার যোগ ও status পরিবর্তনের রেকর্ড
setActivityUser(null)} className="px-3 py-1.5 border rounded">বন্ধ
{(activity.data || []).map(log => { let details = {}; try { details = typeof log.details === 'string' ? JSON.parse(log.details) : (log.details || {}); } catch (e) {} return
{eventLabel(log.event_type)} {new Date(log.created_at).toLocaleString()}
কর্তা: {log.actor_username || (log.actor_id ? `ID ${log.actor_id}` : 'System / পুরোনো রেকর্ড')}
{details.disable_at &&
Disable date: {new Date(details.disable_at).toLocaleString()}
}
; })}
{activity.data?.length === 0 &&
এখনো কোনো activity নেই। Migration-এর আগের activation/deactivation history পাওয়া যাবে না।
}
{activity.last_page > 1 &&
setActivity(a => ({ ...a, current_page: a.current_page - 1 }))} className="px-3 py-1 border rounded disabled:opacity-40">‹ আগের পৃষ্ঠা {toBengaliNumber(activity.current_page)} / {toBengaliNumber(activity.last_page)} = activity.last_page} onClick={() => setActivity(a => ({ ...a, current_page: a.current_page + 1 }))} className="px-3 py-1 border rounded disabled:opacity-40">পরের ›
}
}
);
}
// --- MAIN APP ---
// =====================================================================
// QUESTION BANK MODULE (separate page: list / add / classes / tags)
// =====================================================================
const BANK_TYPES = [
{ v: 'MCQ', l: 'MCQ (বহুনির্বাচনী)' },
{ v: 'Creative', l: 'সৃজনশীল' },
{ v: 'Short', l: 'সংক্ষিপ্ত' },
{ v: 'Other', l: 'অন্যান্য' },
];
const TYPE_LABEL = { MCQ: 'MCQ', Creative: 'সৃজনশীল', Short: 'সংক্ষিপ্ত', Other: 'অন্যান্য' };
const TYPE_COLOR = { MCQ: 'bg-sky-100 text-sky-800 border-sky-200', Creative: 'bg-violet-100 text-violet-800 border-violet-200', Short: 'bg-amber-100 text-amber-800 border-amber-200', Other: 'bg-slate-100 text-slate-700 border-slate-200' };
const DIFFICULTIES = [{ v: 'easy', l: 'সহজ' }, { v: 'medium', l: 'মাঝারি' }, { v: 'hard', l: 'কঠিন' }];
const DIFF_LABEL = { easy: 'সহজ', medium: 'মাঝারি', hard: 'কঠিন' };
const SEC_TO_TYPE = { mcq: 'MCQ', creative: 'Creative', short: 'Short' };
const cloneJson = (o) => JSON.parse(JSON.stringify(o));
const hasContent = (html, img) => plainText(html).length > 0 || !!img;
const readBankView = () => { try { return localStorage.getItem('qm_bank_view') === 'card' ? 'card' : 'list'; } catch (e) { return 'list'; } };
function questionBlockBounds(items, index) {
for (let start = index; start >= 0; start--) {
const head = items[start] || {};
const count = Math.max(1, Number(head.stimulusQCount) || 2);
if ((head.showStimulus || head.sharedStimulus || head.stimulusImage) && index < start + count) return [start, Math.min(items.length, start + count)];
}
return [index, index + 1];
}
const DEFAULT_FILTERS = {
q: '', class_id: '', subject_id: '', subject_code: '', chapter_id: '', topic_id: '', q_type: '', mcq_standard: '', difficulty: '',
marks_min: '', marks_max: '', tag_ids: [], tag_mode: 'or', saved: 'all',
created_from: '', created_to: '', updated_from: '', updated_to: '',
sort: 'created_at', dir: 'desc', page: 1, page_size: 10,
};
async function bankApi(action, body) {
try {
const res = await fetch(`${API_URL}${action}`, body === undefined ? {} : { method: 'POST', body: JSON.stringify(body) });
const txt = await res.text();
let data;
try { data = JSON.parse(txt); }
catch (pe) { return { status: 'error', message: 'সার্ভার থেকে ভুল উত্তর এসেছে (HTTP ' + res.status + '): ' + txt.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 300) }; }
if (data && data.message === 'Unauthorized') alert('সেশন শেষ হয়ে গেছে, আবার লগইন করুন।');
return data;
} catch (e) { return { status: 'error', message: 'সার্ভারের সাথে যোগাযোগ করা যায়নি (ইন্টারনেট/সার্ভার): ' + (e && e.message ? e.message : '') }; }
}
function QImage({ src, cls = 'max-h-28' }) {
if (!src) return null;
return
` }} />;
}
function SourceMetadata({ item }) {
const source = item?.sourceMeta;
if (!source) return null;
const chapter = source.chapter || 'Chapter Not Assigned';
return
ব্যাংক: {source.subject || 'বিষয় নেই'} › {chapter}{source.bankId ? ` · ID ${source.bankId.slice(-8)}` : ''}
;
}
// ---------- blank item factories ----------
function newOptions(stmt) {
const base = stmt ? ['i ও ii', 'i ও iii', 'ii ও iii', 'i, ii ও iii'] : ['', '', '', ''];
return base.map(t => ({ id: generateId('opt'), text: t, widthMode: 'auto' }));
}
function newMCQ(type = 'standard') {
return {
id: generateId('mcq'), type, text: '', image: null,
statements: type === 'statement' ? ['', '', ''] : undefined, statementLayoutMode: 'auto',
subPrompt: type === 'statement' ? 'নিচের কোনটি সঠিক?' : undefined,
marks: 1, correctIndex: null, layoutMode: 'auto', options: newOptions(type === 'statement'),
};
}
function newGroup() { return { isGroup: true, sharedStimulus: '', stimulusImage: null, stimulusInstruction: '', questions: [newMCQ(), newMCQ()] }; }
function newCreative() {
return { id: generateId('cq'), stimulus: '', image: null, subQuestions: [{ label: 'ক', text: '', marks: 1 }, { label: 'খ', text: '', marks: 2 }, { label: 'গ', text: '', marks: 3 }, { label: 'ঘ', text: '', marks: 4 }] };
}
const cqMarks = (cq) => (cq.subQuestions || []).reduce((s, q) => s + (Number(q.marks) || 0), 0);
const normMCQ = (q) => ({ ...newMCQ(q.type === 'statement' ? 'statement' : 'standard'), ...q, options: (q.options && q.options.length ? q.options : newOptions(false)), correctIndex: (q.correctIndex === undefined ? null : q.correctIndex) });
// ---------- single MCQ editor (rich-text options + explicit correct answer) ----------
function BankMCQEditor({ q, onChange, showMarks = true }) {
const set = (patch) => onChange({ ...q, ...patch });
const opts = q.options || [];
const setOpt = (i, patch) => set({ options: opts.map((o, j) => j === i ? { ...o, ...patch } : o) });
const moveOpt = (i, d) => {
const t = i + d; if (t < 0 || t >= opts.length) return;
const next = [...opts]; [next[i], next[t]] = [next[t], next[i]];
let ci = q.correctIndex; if (ci === i) ci = t; else if (ci === t) ci = i;
set({ options: next, correctIndex: ci });
};
const removeOpt = (i) => {
if (opts.length <= 2) return;
let ci = q.correctIndex;
if (ci === i) ci = null; else if (ci !== null && ci !== undefined && ci > i) ci = ci - 1;
set({ options: opts.filter((_, j) => j !== i), correctIndex: ci });
};
const addOpt = () => { if (opts.length >= 6) return; set({ options: [...opts, { id: generateId('opt'), text: '', widthMode: 'auto' }] }); };
const changeType = (type) => {
if (type === q.type) return;
if (type === 'statement') {
const allEmpty = opts.every(o => !plainText(o.text));
set({ type, statements: (q.statements && q.statements.length) ? q.statements : ['', '', ''], subPrompt: q.subPrompt || 'নিচের কোনটি সঠিক?', options: allEmpty ? newOptions(true) : opts, correctIndex: allEmpty ? null : q.correctIndex });
} else {
set({ type: 'standard', statements: undefined, subPrompt: undefined });
}
};
const noAnswer = q.correctIndex === null || q.correctIndex === undefined;
return (
{[['standard', 'সাধারণ MCQ'], ['statement', 'বহুপদী MCQ']].map(([v, l]) => (
changeType(v)} className={`px-3 py-1 rounded-md font-bold ${(q.type || 'standard') === v ? 'bg-emerald-700 text-white' : 'text-slate-600 hover:bg-slate-200'}`}>{l}
))}
{showMarks && (
মান
set({ marks: e.target.value === '' ? '' : Number(e.target.value) })} className="w-16 border rounded px-2 py-1 text-sm text-center outline-none focus:border-emerald-500" />
)}
প্রশ্ন
set({ text: v })} placeholder="প্রশ্ন লিখুন..." minHeight="50px" />
{q.type === 'statement' && (
)}
set({ image: img })} />
অপশনসমূহ ({toBengaliNumber(opts.length)}টি)
{noAnswer
? ⚠ সঠিক উত্তর নির্বাচন করা হয়নি — যে অপশনটি সঠিক তার পাশে “সঠিক করুন” চাপুন
: ✓ সঠিক উত্তর: {BN_OPTIONS[q.correctIndex]} }
{opts.map((o, i) => {
const isCorrect = q.correctIndex === i;
return (
{BN_OPTIONS[i]}
setOpt(i, { text: v })} compact minHeight="34px" placeholder={`অপশন ${BN_OPTIONS[i]} লিখুন...`} />
set({ correctIndex: i })} className={`px-2 py-1 rounded text-[11px] font-bold border whitespace-nowrap ${isCorrect ? 'bg-emerald-600 text-white border-emerald-600' : 'bg-white text-slate-600 border-slate-300 hover:border-emerald-500 hover:text-emerald-700'}`}>{isCorrect ? '✓ সঠিক উত্তর' : 'সঠিক করুন'}
moveOpt(i, -1)} disabled={i === 0} className="px-1.5 py-1 text-[11px] rounded border bg-white hover:bg-slate-100 disabled:opacity-30">↑
moveOpt(i, 1)} disabled={i === opts.length - 1} className="px-1.5 py-1 text-[11px] rounded border bg-white hover:bg-slate-100 disabled:opacity-30">↓
removeOpt(i)} disabled={opts.length <= 2} className="px-1.5 py-1 text-[11px] rounded border border-rose-200 text-rose-600 bg-white hover:bg-rose-50 disabled:opacity-30" title="অপশন মুছুন">✕
);
})}
= 6} className="mt-2 px-3 py-1.5 text-xs font-bold rounded-lg bg-slate-100 hover:bg-slate-200 disabled:opacity-40">+ অপশন যোগ করুন
);
}
// ---------- MCQ group (one shared stimulus, several MCQs) ----------
function BankGroupEditor({ group, onChange }) {
const askConfirm = React.useContext(ConfirmCtx);
const [closed, setClosed] = useState({});
const qs = group.questions || [];
const setQ = (i, q) => onChange({ ...group, questions: qs.map((x, j) => j === i ? q : x) });
const move = (i, d) => { const t = i + d; if (t < 0 || t >= qs.length) return; const n = [...qs]; [n[i], n[t]] = [n[t], n[i]]; onChange({ ...group, questions: n }); };
const remove = (i) => askConfirm({ title: 'MCQ সরাবেন?', message: `গ্রুপ থেকে MCQ ${toBengaliNumber(i + 1)} সরিয়ে ফেলা হবে।`, onConfirm: () => onChange({ ...group, questions: qs.filter((_, j) => j !== i) }) });
return (
{qs.map((q, i) => {
const isClosed = closed[q.id];
return (
setClosed(c => ({ ...c, [q.id]: !c[q.id] }))} className="w-6 h-6 flex items-center justify-center bg-slate-100 rounded text-slate-500 font-bold shrink-0">{isClosed ? '▶' : '▼'}
MCQ {toBengaliNumber(i + 1)}{plainText(q.text) ? ` — ${plainText(q.text).slice(0, 30)}` : ''}
move(i, -1)} disabled={i === 0} className="bg-slate-100 px-2 py-1 rounded hover:bg-slate-200 disabled:opacity-30">↑
move(i, 1)} disabled={i === qs.length - 1} className="bg-slate-100 px-2 py-1 rounded hover:bg-slate-200 disabled:opacity-30">↓
remove(i)} disabled={qs.length <= 1} className="bg-rose-50 text-rose-600 px-2 py-1 rounded hover:bg-rose-100 disabled:opacity-30">✕
{!isClosed &&
setQ(i, nq)} />}
);
})}
onChange({ ...group, questions: [...qs, newMCQ()] })} className="flex-1 bg-emerald-700 text-white text-xs py-2.5 rounded-lg font-bold hover:bg-emerald-800">+ গ্রুপে সাধারণ MCQ যোগ
onChange({ ...group, questions: [...qs, newMCQ('statement')] })} className="flex-1 bg-slate-800 text-white text-xs py-2.5 rounded-lg font-bold hover:bg-slate-700">+ গ্রুপে বহুপদী MCQ যোগ
);
}
function BankCreativeEditor({ cq, onChange }) {
const setSub = (i, patch) => onChange({ ...cq, subQuestions: cq.subQuestions.map((s, j) => j === i ? { ...s, ...patch } : s) });
return (
উদ্দীপক
onChange({ ...cq, stimulus: v })} placeholder="উদ্দীপক লিখুন..." minHeight="70px" />
onChange({ ...cq, image: img })} />
উপ-প্রশ্নসমূহ (ক, খ, গ, ঘ) — মোট মান: {toBengaliNumber(cqMarks(cq))}
{cq.subQuestions.map((s, i) => (
))}
);
}
function BankSimpleEditor({ simple, onChange, label }) {
return (
{label}
onChange({ ...simple, text: v })} placeholder="প্রশ্ন লিখুন..." minHeight="70px" />
onChange({ ...simple, image: img })} />
);
}
// ---------- select with inline "+ add new" ----------
function TaxField({ label, value, onChange, options, onAdd, withCode, disabled, optional, hint }) {
const [open, setOpen] = useState(false);
const [name, setName] = useState('');
const [code, setCode] = useState('');
const [busy, setBusy] = useState(false);
const submit = async () => {
if (!name.trim() || busy) return;
setBusy(true);
const ok = await onAdd(name.trim(), code.trim());
setBusy(false);
if (ok) { setName(''); setCode(''); setOpen(false); }
};
return (
);
}
// ---------- tag picker ----------
function TagPicker({ meta, value, onChange }) {
const [text, setText] = useState('');
const lower = text.trim().toLowerCase();
const toggle = (name) => onChange(value.some(v => v.toLowerCase() === name.toLowerCase()) ? value.filter(v => v.toLowerCase() !== name.toLowerCase()) : [...value, name]);
const addTyped = () => { const t = text.trim(); if (!t) return; if (!value.some(v => v.toLowerCase() === t.toLowerCase())) onChange([...value, t]); setText(''); };
const suggestions = meta.tags.filter(t => !value.some(v => v.toLowerCase() === t.name.toLowerCase()) && (!lower || t.name.toLowerCase().includes(lower))).slice(0, 30);
return (
ট্যাগ (একাধিক যুক্ত করা যাবে)
{value.map(t => #{t} toggle(t)} className="w-4 h-4 rounded-full hover:bg-emerald-200 leading-none">× )}
setText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); addTyped(); } }} placeholder="ট্যাগ লিখে Enter চাপুন..." className="flex-1 min-w-[160px] outline-none text-sm px-1 py-0.5" />
{suggestions.length > 0 && (
{suggestions.map(t => toggle(t.name)} className="px-2 py-0.5 rounded-full text-[11px] border border-slate-200 bg-slate-50 hover:border-emerald-500 hover:bg-emerald-50">+ {t.name} )}
)}
);
}
// ---------- ADD / EDIT FORM ----------
function BankAddForm({ meta, reloadMeta, editing, prefill, hint, onDone, showToast }) {
const blankForm = () => ({ q_type: 'MCQ', class_id: '', subject_id: '', subject_code: '', chapter_id: '', topic_id: '', difficulty: '', marks: 1, tags: [] });
const [form, setForm] = useState(blankForm);
const [mode, setMode] = useState('single');
const [mcq, setMcq] = useState(() => newMCQ());
const [group, setGroup] = useState(() => newGroup());
const [cq, setCq] = useState(() => newCreative());
const [simple, setSimple] = useState({ text: '', image: null });
const [marksTouched, setMarksTouched] = useState(false);
const [errors, setErrors] = useState([]);
const [busy, setBusy] = useState(false);
const [resetKey, setResetKey] = useState(0);
const topRef = useRef(null);
const isEdit = !!editing;
const clearContent = () => { setMcq(newMCQ()); setGroup(newGroup()); setCq(newCreative()); setSimple({ text: '', image: null }); setErrors([]); setResetKey(k => k + 1); };
const loadItem = (qType, item) => {
if (qType === 'MCQ') {
if (item && item.isGroup) { setMode('group'); setGroup({ ...newGroup(), ...cloneJson(item), questions: (item.questions || []).map(q => normMCQ(cloneJson(q))) }); }
else if (item && plainText(item.sharedStimulus)) {
const { sharedStimulus, stimulusImage, stimulusInstruction, showStimulus, stimulusQCount, ...rest } = cloneJson(item);
setMode('group'); setGroup({ isGroup: true, sharedStimulus, stimulusImage: stimulusImage || null, stimulusInstruction: stimulusInstruction || '', questions: [normMCQ({ ...rest, id: generateId('mcq') })] });
} else { setMode('single'); setMcq(normMCQ(cloneJson(item || {}))); }
} else if (qType === 'Creative') {
const base = newCreative(); const it = cloneJson(item || {});
setCq({ ...base, ...it, subQuestions: (it.subQuestions && it.subQuestions.length ? it.subQuestions : base.subQuestions) });
} else { setSimple({ text: (item && item.text) || '', image: (item && item.image) || null }); }
};
useEffect(() => {
setErrors([]);
if (editing) {
setForm({ q_type: editing.q_type, class_id: editing.class_id || '', subject_id: editing.subject_id || '', subject_code: editing.subject_code || '', chapter_id: editing.chapter_id || '', topic_id: editing.topic_id || '', difficulty: editing.difficulty || '', marks: editing.marks || 1, tags: (editing.tags || []).map(t => t.name) });
setMarksTouched(true); loadItem(editing.q_type, editing.item); setResetKey(k => k + 1);
} else if (prefill) {
const qType = SEC_TO_TYPE[prefill.secKey] || 'Short';
const f = { ...blankForm(), q_type: qType, marks: prefill.item?.marks || (qType === 'MCQ' ? 1 : qType === 'Creative' ? 10 : 2) };
if (hint) {
const c = meta.classes.find(x => x.name === (hint.className || '').trim());
if (c) { f.class_id = c.id; const s = meta.subjects.find(x => x.class_id === c.id && x.name === (hint.subject || '').trim()); if (s) { f.subject_id = s.id; f.subject_code = s.code || ''; } }
}
setForm(f); setMarksTouched(false); loadItem(qType, prefill.item); setResetKey(k => k + 1);
} else { setForm(blankForm()); setMode('single'); setMarksTouched(false); clearContent(); }
}, [editing, prefill]);
// Creative: keep question-level marks in sync with sub-question marks unless edited by hand
useEffect(() => { if (form.q_type === 'Creative' && !marksTouched) setForm(f => ({ ...f, marks: cqMarks(cq) })); }, [cq, form.q_type, marksTouched]);
const subjects = meta.subjects.filter(s => s.class_id === Number(form.class_id));
const chapters = meta.chapters.filter(c => c.subject_id === Number(form.subject_id));
const topics = meta.topics.filter(t => t.chapter_id === Number(form.chapter_id));
const groupMarks = (group.questions || []).reduce((s, q) => s + (Number(q.marks) || 0), 0);
const setClass = (id) => setForm(f => ({ ...f, class_id: id, subject_id: '', subject_code: '', chapter_id: '', topic_id: '' }));
const setSubject = (id) => setForm(f => { const s = meta.subjects.find(x => x.id === id); return { ...f, subject_id: id, subject_code: s ? (s.code || '') : '', chapter_id: '', topic_id: '' }; });
const setChapter = (id) => setForm(f => ({ ...f, chapter_id: id, topic_id: '' }));
const changeType = (t) => {
if (isEdit || t === form.q_type) return;
setMarksTouched(false);
setForm(f => ({ ...f, q_type: t, marks: t === 'MCQ' ? 1 : t === 'Creative' ? cqMarks(cq) : t === 'Short' ? 2 : 1 }));
setErrors([]);
};
const quickAdd = async (type, parent_id, name, code, after) => {
const res = await bankApi('taxSave', { type, parent_id, name, code });
if (res.status !== 'success') { showToast(res.message || 'যোগ করা যায়নি'); return false; }
await reloadMeta(); after(res.id); return true;
};
const validate = (isSaved) => {
const e = [];
if (isSaved) {
if (!form.class_id) e.push('শ্রেণি নির্বাচন করুন');
if (!form.subject_id) e.push('বিষয় নির্বাচন করুন');
if (!form.chapter_id) e.push('অধ্যায় নির্বাচন করুন');
}
const checkMcq = (q, p) => {
if (!hasContent(q.text, q.image)) e.push(`${p}প্রশ্ন লিখুন`);
if (isSaved) {
if (q.type === 'statement' && (q.statements || []).filter(s => plainText(s)).length < 2) e.push(`${p}কমপক্ষে ২টি বিবৃতি লিখুন`);
if ((q.options || []).length < 2 || (q.options || []).some(o => !plainText(o.text))) e.push(`${p}সব অপশন পূরণ করুন (কমপক্ষে ২টি)`);
if (q.correctIndex === null || q.correctIndex === undefined) e.push(`${p}সঠিক উত্তর নির্বাচন করুন`);
}
};
if (form.q_type === 'MCQ') {
if (mode === 'group') {
if (!hasContent(group.sharedStimulus, group.stimulusImage)) e.push('গ্রুপের অভিন্ন উদ্দীপক লিখুন');
if (isSaved && group.questions.length < 2) e.push('গ্রুপে কমপক্ষে ২টি MCQ থাকতে হবে');
group.questions.forEach((q, i) => checkMcq(q, `MCQ ${toBengaliNumber(i + 1)}: `));
} else checkMcq(mcq, '');
} else if (form.q_type === 'Creative') {
if (!hasContent(cq.stimulus, cq.image)) e.push('উদ্দীপক লিখুন');
if (isSaved && cq.subQuestions.some(s => !plainText(s.text))) e.push('ক, খ, গ, ঘ — সব উপ-প্রশ্ন লিখুন');
if (!isSaved && !hasContent(cq.stimulus, cq.image) && cq.subQuestions.every(s => !plainText(s.text))) e.push('অন্তত কিছু লিখুন');
} else if (!hasContent(simple.text, simple.image)) e.push('প্রশ্ন লিখুন');
return e;
};
const buildItem = () => {
if (form.q_type === 'MCQ') {
if (mode === 'group') return { ...group, isGroup: true, questions: group.questions.map(q => ({ ...q, marks: Number(q.marks) || 1 })) };
return { ...mcq, marks: Number(form.marks) || 1 };
}
if (form.q_type === 'Creative') return cq;
return { id: generateId(form.q_type === 'Short' ? 'sq' : 'oq'), text: simple.text, image: simple.image, marks: Number(form.marks) || 0, showIndividualMarks: false };
};
const save = async (isSaved, goList) => {
const errs = validate(isSaved);
setErrors(errs);
if (errs.length) { topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); return; }
setBusy(true);
const res = await bankApi('bankSave', {
bankId: isEdit ? editing.bankId : undefined, q_type: form.q_type,
class_id: form.class_id || null, subject_id: form.subject_id || null, chapter_id: form.chapter_id || null, topic_id: form.topic_id || null,
subject_code: form.subject_code, difficulty: form.difficulty, marks: form.q_type === 'MCQ' && mode === 'group' ? groupMarks : Number(form.marks) || 0,
tags: form.tags, is_saved: isSaved, item: buildItem(),
});
setBusy(false);
if (res.status !== 'success') { setErrors([res.message || 'সংরক্ষণ ব্যর্থ']); topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); return; }
await reloadMeta();
showToast(isSaved ? '✓ প্রশ্ন সেভ হয়েছে' : 'প্রশ্নটি আনসেভড (ড্রাফট) হিসেবে রাখা হয়েছে');
if (isEdit || prefill || goList) onDone(); else { clearContent(); setMarksTouched(false); topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
};
return (
{isEdit ? '✏️ প্রশ্ন সম্পাদনা' : '➕ নতুন প্রশ্ন যোগ করুন'}
{prefill && !isEdit && প্রশ্নপত্র থেকে আনা হয়েছে — শ্রেণি/বিষয়/অধ্যায় বাছাই করে সেভ করুন }
{errors.length > 0 && (
সেভ করার আগে এগুলো ঠিক করুন:
{errors.map((e, i) => {e} )}
)}
প্রশ্নের ধরন {isEdit && (সম্পাদনার সময় পরিবর্তন করা যায় না) }
{BANK_TYPES.map(t => (
changeType(t.v)} disabled={isEdit && form.q_type !== t.v} className={`px-4 py-2 rounded-lg text-sm font-bold border transition ${form.q_type === t.v ? 'bg-emerald-700 text-white border-emerald-700 shadow-sm' : 'bg-white text-slate-600 hover:bg-slate-50 disabled:opacity-40'}`}>{t.l}
))}
quickAdd('class', null, n, '', (id) => setClass(id))} />
quickAdd('subject', form.class_id, n, c, (id) => setForm(f => ({ ...f, subject_id: id, subject_code: c || '', chapter_id: '', topic_id: '' })))} />
বিষয় কোড (ফ্রি-টেক্সট, ঐচ্ছিক)
setForm(f => ({ ...f, subject_code: e.target.value }))} list="bank-code-list" placeholder="যেমন: ১২৭ / SCI-07" className="w-full border rounded p-2 text-sm outline-none focus:ring-1 focus:ring-emerald-500" />
{(meta.codes || []).map(c => )}
quickAdd('chapter', form.subject_id, n, '', (id) => setChapter(id))} />
setForm(f => ({ ...f, topic_id: id }))} options={topics} disabled={!form.chapter_id} hint="আগে অধ্যায় বাছাই করুন" onAdd={(n) => quickAdd('topic', form.chapter_id, n, '', (id) => setForm(f => ({ ...f, topic_id: id })))} />
setForm(f => ({ ...f, tags }))} />
{form.q_type === 'MCQ' && (
<>
setMode('single')} disabled={isEdit} className={`px-3 py-1.5 rounded-md font-bold ${mode === 'single' ? 'bg-slate-800 text-white' : 'text-slate-600 hover:bg-slate-200'} disabled:opacity-50`}>একক MCQ
setMode('group')} disabled={isEdit} className={`px-3 py-1.5 rounded-md font-bold ${mode === 'group' ? 'bg-amber-600 text-white' : 'text-slate-600 hover:bg-slate-200'} disabled:opacity-50`}>MCQ গ্রুপ (অভিন্ন উদ্দীপক)
গ্রুপ শুধু MCQ-এর জন্য — সৃজনশীল ও সংক্ষিপ্ত প্রশ্নে গ্রুপ নেই।
{mode === 'group' ?
:
}
>
)}
{form.q_type === 'Creative' &&
}
{form.q_type === 'Short' &&
}
{form.q_type === 'Other' &&
}
save(true, false)} className="px-4 py-2.5 rounded-lg bg-emerald-700 text-white text-sm font-bold hover:bg-emerald-800 disabled:opacity-50">{isEdit ? '✓ সেভ করুন' : '✓ সেভ করুন ও আরেকটি যোগ করুন'}
{!isEdit && save(true, true)} className="px-4 py-2.5 rounded-lg bg-slate-800 text-white text-sm font-bold hover:bg-slate-700 disabled:opacity-50">সেভ করে তালিকায় যান }
save(false, isEdit || !!prefill)} className="px-4 py-2.5 rounded-lg border border-amber-400 bg-amber-50 text-amber-800 text-sm font-bold hover:bg-amber-100 disabled:opacity-50">আনসেভড (ড্রাফট) রাখুন
{(isEdit || prefill) && বাতিল }
{busy && সংরক্ষণ হচ্ছে... }
);
}
// ---------- question card preview ----------
function MCQBody({ q, expanded }) {
const opts = q.options || [];
const roman = ['i', 'ii', 'iii', 'iv', 'v', 'vi'];
return (
{expanded &&
}
{expanded && q.type === 'statement' && (
{(q.statements || []).map((s, i) =>
{roman[i]}.
)}
{q.subPrompt &&
{q.subPrompt}
}
)}
{expanded ? (
{opts.map((o, i) => (
{BN_OPTIONS[i]}. {q.correctIndex === i && ✓ }
))}
) : (
{q.type === 'statement' ? 'বহুপদী · ' : ''}অপশন {toBengaliNumber(opts.length)}টি · সঠিক উত্তর: {q.correctIndex === null || q.correctIndex === undefined ? নির্বাচিত নয় : BN_OPTIONS[q.correctIndex]}
)}
);
}
function EntryPreview({ entry, expanded }) {
const it = entry.item || {};
if (entry.q_type === 'MCQ' && it.isGroup) {
const qs = it.questions || [];
return (
📄 অভিন্ন উদ্দীপক · {toBengaliNumber(qs.length)}টি MCQ
{it.stimulusInstruction && expanded &&
{it.stimulusInstruction}
}
{expanded &&
}
{expanded && qs.map((q, i) =>
{toBengaliNumber(i + 1)}.
)}
);
}
if (entry.q_type === 'MCQ') return
;
if (entry.q_type === 'Creative') {
return (
{expanded &&
}
{expanded &&
{(it.subQuestions || []).map((s, i) =>
{s.label}.
{toBengaliNumber(s.marks)} )}
}
);
}
return (
);
}
function fmtDate(s) { if (!s) return ''; const d = new Date(String(s).replace(' ', 'T')); return isNaN(d) ? s : d.toLocaleDateString('bn-BD', { day: 'numeric', month: 'short', year: 'numeric' }); }
function BankCard({ entry, selected, onSelect, expanded, onToggle, onEdit, onDuplicate, onMove, onToggleSaved, onDelete, onInsert, onTagClick, picker, permissions = {} }) {
const it = entry.item || {};
const empty = !entry.class_name;
return (
{TYPE_LABEL[entry.q_type] || entry.q_type}{entry.is_group ? ' গ্রুপ' : ''}
{empty ? শ্রেণিবিহীন : {entry.class_name} }
{entry.subject && {entry.subject}{entry.subject_code ? ` · ${entry.subject_code}` : ''} }
navigator.clipboard?.writeText(entry.bankId)} title={entry.bankId} className="px-2 py-0.5 rounded border border-slate-200 bg-white text-slate-500 font-mono">ID {entry.bankId.slice(-8)} ⧉
{entry.difficulty && {DIFF_LABEL[entry.difficulty]} }
মান {toBengaliNumber(entry.marks)}
{entry.is_saved ? '✓ সেভড' : '● আনসেভড'}
{(entry.chapter || entry.topic) &&
{entry.chapter}{entry.topic ? ` › ${entry.topic}` : ''}
}
{entry.tags.length > 0 &&
{entry.tags.map(t => onTagClick(t.id)} className="text-[11px] px-2 py-0.5 rounded-full bg-slate-100 border border-slate-200 text-slate-600 hover:bg-emerald-50 hover:border-emerald-400">#{t.name} )}
}
তৈরি: {fmtDate(entry.created_at)} · সংশোধন: {fmtDate(entry.updated_at)}
{expanded ? '▲ সংক্ষিপ্ত' : (entry.is_group ? '▼ গ্রুপ খুলুন' : '▼ বিস্তারিত')}
+ পরীক্ষায় যুক্ত করুন
{!picker && permissions.edit && <>✏️ সম্পাদনা
📂 স্থানান্তর
{entry.is_saved ? 'আনসেভড করুন' : 'সেভড করুন'} >}
{!picker && permissions.add && 📋 ডুপ্লিকেট }
{!picker && permissions.add && !permissions.edit && 📂 কপি }
{!picker && permissions.delete && ✕ মুছুন }
);
}
function MoveModal({ meta, ids, onClose, onDone, showToast, canEdit, canAdd }) {
const [c, setC] = useState(''); const [s, setS] = useState(''); const [ch, setCh] = useState(''); const [t, setT] = useState('');
const [busy, setBusy] = useState(false);
const go = async (copy = false) => {
if (!c || !s || !ch) { showToast('শ্রেণি, বিষয় ও অধ্যায় নির্বাচন করুন'); return; }
setBusy(true);
const res = await bankApi(copy ? 'bankCopy' : 'bankMove', { bankIds: ids, class_id: c, subject_id: s, chapter_id: ch, topic_id: t || null });
setBusy(false);
if (res.status === 'success') { showToast(`${toBengaliNumber(copy ? res.copied : res.moved)}টি প্রশ্ন ${copy ? 'কপি' : 'স্থানান্তর'} হয়েছে`); onDone(); } else showToast(res.message || 'স্থানান্তর ব্যর্থ');
};
return (
e.stopPropagation()} className="bg-white rounded-2xl shadow-2xl w-full max-w-md p-5 space-y-3">
📂 {toBengaliNumber(ids.length)}টি প্রশ্ন স্থানান্তর করুন
{ setC(v); setS(''); setCh(''); setT(''); }} options={meta.classes} />
{ setS(v); setCh(''); setT(''); }} options={meta.subjects.filter(x => x.class_id === Number(c))} disabled={!c} hint="আগে শ্রেণি বাছাই করুন" />
{ setCh(v); setT(''); }} options={meta.chapters.filter(x => x.subject_id === Number(s))} disabled={!s} hint="আগে বিষয় বাছাই করুন" />
x.chapter_id === Number(ch))} disabled={!ch} hint="আগে অধ্যায় বাছাই করুন" />
বাতিল
{canEdit && go(false)} disabled={busy} className="px-4 py-2 text-sm font-bold rounded-lg bg-emerald-700 text-white hover:bg-emerald-800 disabled:opacity-50">স্থানান্তর করুন }
{canAdd && go(true)} disabled={busy} className="px-4 py-2 text-sm font-bold rounded-lg bg-blue-700 text-white hover:bg-blue-800 disabled:opacity-50">কপি করুন }
);
}
function BulkEditModal({ meta, ids, onClose, onDone, showToast }) {
const [values, setValues] = useState({ class_id: '', subject_id: '', chapter_id: '', topic_id: '', difficulty: '' });
const [busy, setBusy] = useState(false);
const field = (label, key, options) =>
{label} setValues(v => ({ ...v, [key]: e.target.value }))}>অপরিবর্তিত {options.map(o => {o.name} )} ;
const save = async () => {
const changes = {};
['class_id', 'subject_id', 'chapter_id', 'topic_id'].forEach(k => { if (values[k]) changes[k] = Number(values[k]); });
if (values.difficulty) changes.difficulty = values.difficulty === '__clear__' ? null : values.difficulty;
if (!Object.keys(changes).length) { showToast('অন্তত একটি field নির্বাচন করুন'); return; }
setBusy(true); const res = await bankApi('bankBulkEdit', { bankIds: ids, changes }); setBusy(false);
if (res.status === 'success') { showToast(`${toBengaliNumber(res.updated)}টি প্রশ্ন আপডেট হয়েছে`); onDone(); } else showToast(res.message || 'Bulk edit ব্যর্থ');
};
return
e.stopPropagation()} className="bg-white rounded-2xl shadow-2xl w-full max-w-md p-5 space-y-3">
{toBengaliNumber(ids.length)}টি প্রশ্ন Bulk Edit {field('শ্রেণি', 'class_id', meta.classes)}{field('বিষয়', 'subject_id', meta.subjects)}{field('অধ্যায়', 'chapter_id', meta.chapters)}{field('টপিক', 'topic_id', meta.topics)}
কঠিনতা setValues(v => ({ ...v, difficulty: e.target.value }))}>অপরিবর্তিত সহজ মাঝারি কঠিন মুছুন বাতিল পরিবর্তন করুন
;
}
// ---------- LIST with server-side filters ----------
function BankList({ meta, reloadMeta, filters, setFilters, listVersion, bump, onEdit, onInsert, showToast, activePaperTitle, picker, permissions = {} }) {
const canAdd = permissions.can_add_question_bank;
const canEdit = permissions.can_edit_question_bank;
const canDelete = permissions.can_delete_question_bank;
const askConfirm = React.useContext(ConfirmCtx);
const f = filters;
const [data, setData] = useState({ items: [], total: 0, pages: 1, page: 1 });
const [loading, setLoading] = useState(true);
const [err, setErr] = useState('');
const [showFilters, setShowFilters] = useState(true);
const [selected, setSelected] = useState({});
const [expanded, setExpanded] = useState({});
const [moveIds, setMoveIds] = useState(null);
const [editIds, setEditIds] = useState(null);
const [busy, setBusy] = useState('');
const [report, setReport] = useState(null);
const [importDraft, setImportDraft] = useState(null);
const [viewMode, setViewMode] = useState(readBankView);
const fileRef = useRef(null);
const reqRef = useRef(0);
const patch = (p) => setFilters(cur => ({ ...cur, page: 1, ...p }));
useEffect(() => { try { localStorage.setItem('qm_bank_view', viewMode); } catch (e) {} }, [viewMode]);
const key = JSON.stringify(f);
useEffect(() => {
const my = ++reqRef.current; setLoading(true);
const timer = setTimeout(async () => {
const d = await bankApi('bankList', { filters: f });
if (my !== reqRef.current) return;
setLoading(false);
if (d.status === 'success') { setErr(''); setData(d); if (d.page !== f.page) setFilters(cur => ({ ...cur, page: d.page })); }
else setErr(d.message || 'তালিকা লোড করা যায়নি');
}, 250);
return () => clearTimeout(timer);
}, [key, listVersion]);
const selIds = Object.keys(selected).filter(k => selected[k]);
const byId = (id) => data.items.find(x => x.bankId === id);
const refresh = async () => { bump(); await reloadMeta(); };
// cascading option lists (independent clearing is fine because filters use ids)
const clsName = (id) => (meta.classes.find(x => x.id === id) || {}).name || '';
const subOpts = meta.subjects.filter(s => !f.class_id || Number(s.class_id) === Number(f.class_id)).map(s => ({ ...s, name: f.class_id ? s.name : `${clsName(s.class_id)} › ${s.name}`, code: '' }));
const selectedSubjectIds = new Set(meta.subjects
.filter(s => (!f.class_id || Number(s.class_id) === Number(f.class_id)) && (!f.subject_id || Number(s.id) === Number(f.subject_id)))
.map(s => Number(s.id)));
const chOpts = meta.chapters.filter(c => selectedSubjectIds.has(Number(c.subject_id)))
.map(c => { const s = meta.subjects.find(x => x.id === c.subject_id); return { ...c, name: f.subject_id ? c.name : `${s ? s.name : ''} › ${c.name}` }; });
const selectedChapterIds = new Set(chOpts.map(c => Number(c.id)));
const tpOpts = meta.topics.filter(t => selectedChapterIds.has(Number(t.chapter_id)) && (!f.chapter_id || Number(t.chapter_id) === Number(f.chapter_id)))
.map(t => { const c = meta.chapters.find(x => x.id === t.chapter_id); return { ...t, name: f.chapter_id ? t.name : `${c ? c.name : ''} › ${t.name}` }; });
// active filter chips
const nm = (list, id) => (list.find(x => x.id === Number(id)) || {}).name || id;
const chips = [];
if (f.q) chips.push({ k: 'q', label: `খোঁজ: “${f.q}”`, clear: { q: '' } });
if (f.class_id) chips.push({ k: 'class', label: `শ্রেণি: ${nm(meta.classes, f.class_id)}`, clear: { class_id: '' } });
if (f.subject_id) chips.push({ k: 'subject', label: `বিষয়: ${nm(meta.subjects, f.subject_id)}`, clear: { subject_id: '' } });
if (f.subject_code) chips.push({ k: 'code', label: `বিষয় কোড: ${f.subject_code}`, clear: { subject_code: '' } });
if (f.chapter_id) chips.push({ k: 'chapter', label: `অধ্যায়: ${nm(meta.chapters, f.chapter_id)}`, clear: { chapter_id: '' } });
if (f.topic_id) chips.push({ k: 'topic', label: `টপিক: ${nm(meta.topics, f.topic_id)}`, clear: { topic_id: '' } });
if (f.q_type) chips.push({ k: 'type', label: `ধরন: ${TYPE_LABEL[f.q_type]}`, clear: { q_type: '' } });
if (f.mcq_standard) chips.push({ k: 'standard', label: `MCQ: ${{standard:'Standard MCQ',stimulus:'অভিন্ন উদ্দীপক MCQ',polynomial:'বহুপদী সমাপ্তিসূচক MCQ'}[f.mcq_standard]}`, clear: { mcq_standard: '' } });
if (f.difficulty) chips.push({ k: 'diff', label: `কঠিনতা: ${DIFF_LABEL[f.difficulty]}`, clear: { difficulty: '' } });
if (f.marks_min !== '' || f.marks_max !== '') chips.push({ k: 'marks', label: `মান: ${f.marks_min !== '' ? f.marks_min : '০'} – ${f.marks_max !== '' ? f.marks_max : '∞'}`, clear: { marks_min: '', marks_max: '' } });
(f.tag_ids || []).forEach(id => chips.push({ k: 'tag' + id, label: `#${nm(meta.tags, id)}`, clear: { tag_ids: f.tag_ids.filter(x => x !== id) } }));
if (f.saved !== 'all') chips.push({ k: 'saved', label: f.saved === '1' ? 'শুধু সেভড' : 'শুধু আনসেভড', clear: { saved: 'all' } });
if (f.created_from || f.created_to) chips.push({ k: 'created', label: `তৈরি: ${f.created_from || '…'} → ${f.created_to || '…'}`, clear: { created_from: '', created_to: '' } });
if (f.updated_from || f.updated_to) chips.push({ k: 'updated', label: `সংশোধন: ${f.updated_from || '…'} → ${f.updated_to || '…'}`, clear: { updated_from: '', updated_to: '' } });
const clearAll = () => setFilters(cur => ({ ...DEFAULT_FILTERS, sort: cur.sort, dir: cur.dir, page_size: cur.page_size }));
const doDelete = (ids, label) => askConfirm({ title: 'প্রশ্ন মুছবেন?', message: `${label} প্রশ্ন আর্কাইভ হবে এবং Undo করা যাবে।`, onConfirm: async () => {
const res = await bankApi('bankDelete', { bankIds: ids });
if (res.status === 'success') { showToast(`${toBengaliNumber(res.deleted)}টি প্রশ্ন মুছে ফেলা হয়েছে`, async () => { const undo = await bankApi('bankRestore', { bankIds: res.undoIds }); if (undo.status === 'success') { showToast('প্রশ্ন পুনরুদ্ধার হয়েছে'); refresh(); } }); setSelected({}); refresh(); } else showToast(res.message || 'মুছতে ব্যর্থ');
} });
const bulkTag = async (mode) => {
const value = window.prompt(mode === 'replace' ? 'নতুন ট্যাগগুলো কমা দিয়ে লিখুন' : mode === 'add' ? 'যে ট্যাগগুলো যুক্ত করবেন, কমা দিয়ে লিখুন' : 'যে ট্যাগগুলো সরাবেন, কমা দিয়ে লিখুন');
if (value === null) return;
const res = await bankApi('bankBulkTags', { bankIds: selIds, mode, tags: value.split(',').map(x => x.trim()).filter(Boolean) });
if (res.status === 'success') { showToast('নির্বাচিত প্রশ্নের ট্যাগ আপডেট হয়েছে'); refresh(); } else showToast(res.message || 'ট্যাগ আপডেট ব্যর্থ');
};
const bulkDifficulty = async () => {
const value = window.prompt('Difficulty: easy, medium, hard, অথবা খালি দিলে clear হবে', 'medium');
if (value === null) return;
const difficulty = value.trim();
if (difficulty && !['easy', 'medium', 'hard'].includes(difficulty)) { showToast('easy, medium অথবা hard লিখুন'); return; }
const res = await bankApi('bankBulkEdit', { bankIds: selIds, changes: { difficulty: difficulty || null } });
if (res.status === 'success') { showToast('Difficulty আপডেট হয়েছে'); refresh(); } else showToast(res.message || 'পরিবর্তন ব্যর্থ');
};
const setSaved = async (ids, val) => {
const res = await bankApi('bankSetSaved', { bankIds: ids, is_saved: val });
if (res.status === 'success') { showToast(val ? '✓ সেভড করা হয়েছে' : 'আনসেভড করা হয়েছে'); refresh(); } else showToast(res.message || 'ব্যর্থ');
};
const duplicate = async (id) => { const res = await bankApi('bankDuplicate', { bankId: id }); if (res.status === 'success') { showToast('প্রশ্ন ডুপ্লিকেট হয়েছে'); refresh(); } else showToast(res.message || 'ব্যর্থ'); };
const doExport = async (ids = []) => {
setBusy('export');
const d = await bankApi('bankExport', ids.length ? { bankIds: ids } : { filters: f });
setBusy('');
if (!d || d.format !== 'qmaker-question-bank') { showToast((d && d.message) || 'এক্সপোর্ট ব্যর্থ'); return; }
const blob = new Blob([JSON.stringify(d, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob); const a = document.createElement('a');
a.href = url; a.download = `question-bank-${new Date().toISOString().slice(0, 10)}.json`;
document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000);
showToast(`${toBengaliNumber(d.count)}টি প্রশ্ন এক্সপোর্ট হয়েছে`);
};
const runImport = async (list, duplicateMode = 'skip') => {
setBusy('import'); let imported = 0, skipped = 0, updated = 0, duplicates = 0, failed = 0, errors = [];
for (let i = 0; i < list.length; i += 40) {
const res = await bankApi('bankImport', { questions: list.slice(i, i + 40), duplicate_mode: duplicateMode });
if (res.status !== 'success') { errors.push(res.message || 'ইমপোর্ট ব্যর্থ'); skipped += Math.min(40, list.length - i); continue; }
imported += res.imported; updated += res.updated || 0; duplicates += res.duplicates || 0; failed += res.failed || 0; skipped += res.skipped; errors = errors.concat(res.errors || []);
}
setBusy(''); setReport({ imported, skipped, updated, duplicates, failed, errors: errors.slice(0, 20) }); refresh();
};
const onFile = async (e) => {
const file = e.target.files && e.target.files[0]; e.target.value = '';
if (!file) return;
let parsed;
try { parsed = JSON.parse(await file.text()); } catch (x) { showToast('ফাইলটি সঠিক JSON নয়'); return; }
const list = Array.isArray(parsed) ? parsed : parsed && parsed.questions;
if (!Array.isArray(list) || list.length === 0) { showToast('ফাইলে কোনো প্রশ্ন পাওয়া যায়নি'); return; }
const preview = list.slice(0, 3).map((q, i) => `${i + 1}. ${plainText(q?.item?.text || q?.item?.stimulus || q?.item?.sharedStimulus || '')}`).join('\n');
setImportDraft({ list, preview });
};
const pageNums = (() => { const p = data.page, n = data.pages, out = []; for (let i = Math.max(1, p - 2); i <= Math.min(n, p + 2); i++) out.push(i); return out; })();
const sel = 'w-full border rounded px-2 py-1.5 text-sm outline-none bg-white focus:ring-1 focus:ring-emerald-500';
const lbl = 'block text-[11px] font-bold text-slate-600 mb-0.5';
return (
{importDraft &&
{ if (e.target === e.currentTarget) setImportDraft(null); }}>
📥
প্রশ্ন ইমপোর্ট {toBengaliNumber(importDraft.list.length)}টি প্রশ্ন পাওয়া গেছে। ডুপ্লিকেট পেলে কী করবেন?
{[['skip', 'বাদ দিন', 'ডুপ্লিকেট প্রশ্ন আগের মতোই থাকবে'], ['update', 'আগেরটি আপডেট করুন', 'ডুপ্লিকেট প্রশ্নের তথ্য নতুন ফাইলের তথ্য দিয়ে বদলাবে'], ['new', 'নতুন হিসেবে যোগ করুন', 'ডুপ্লিকেট হলেও আলাদা প্রশ্ন হিসেবে যোগ হবে']].map(([value, title, detail], i) =>
setImportDraft(d => ({ ...d, mode: value }))} className="mt-1 accent-emerald-700" />
{i + 1}. {title} {detail}
)}
{importDraft.preview &&
প্রিভিউ
{importDraft.preview} }
শ্রেণি, বিষয়, অধ্যায় বা ট্যাগ না থাকলে ইমপোর্টের সময় তৈরি হবে।
setImportDraft(null)} className="px-4 py-2 text-sm font-bold rounded-lg border hover:bg-slate-50">বাতিল { const draft = importDraft; setImportDraft(null); runImport(draft.list, draft.mode || 'skip'); }} className="px-4 py-2 text-sm font-bold rounded-lg bg-emerald-700 text-white hover:bg-emerald-800">ইমপোর্ট করুন
}
patch({ q: e.target.value })} placeholder="🔍 ID, প্রশ্ন, অধ্যায়, টপিক বা ট্যাগ দিয়ে খুঁজুন..." className="flex-1 min-w-[220px] border rounded-lg px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-emerald-500" />
setShowFilters(!showFilters)} className="px-3 py-2 rounded-lg border text-xs font-bold hover:bg-slate-50">{showFilters ? '▲ ফিল্টার লুকান' : '▼ ফিল্টার দেখান'}
{!picker && <>{busy === 'export' ? '...' : `⬇ JSON এক্সপোর্ট (${toBengaliNumber(data.total)}টি)`}
doExport(selIds)} disabled={!selIds.length || busy === 'export'} className="px-3 py-2 rounded-lg border text-xs font-bold disabled:opacity-40">নির্বাচিত Export
{canAdd && <>
fileRef.current && fileRef.current.click()} disabled={busy === 'import'} className="px-3 py-2 rounded-lg border border-slate-300 text-xs font-bold hover:bg-slate-50 disabled:opacity-40">{busy === 'import' ? 'ইমপোর্ট হচ্ছে...' : '⬆ JSON ইমপোর্ট'}
>}>}
{showFilters && (
শ্রেণি patch({ class_id: e.target.value ? Number(e.target.value) : '', subject_id: '', chapter_id: '', topic_id: '' })}>সব শ্রেণি {meta.classes.map(c => {c.name} )}
বিষয় patch({ subject_id: e.target.value ? Number(e.target.value) : '', chapter_id: '', topic_id: '' })}>সব বিষয় {subOpts.map(s => {s.name} )}
বিষয় কোড patch({ subject_code: e.target.value })}>সব কোড {(meta.codes || []).map(c => {c} )}
অধ্যায় patch({ chapter_id: e.target.value ? Number(e.target.value) : '', topic_id: '' })}>সব অধ্যায় {chOpts.map(c => {c.name} )}
টপিক patch({ topic_id: e.target.value ? Number(e.target.value) : '' })}>সব টপিক {tpOpts.map(t => {t.name} )}
প্রশ্নের ধরন patch({ q_type: e.target.value })}>সব ধরন {BANK_TYPES.map(t => {TYPE_LABEL[t.v]} )}
MCQ Standard patch({ q_type: 'MCQ', mcq_standard: e.target.value })}>সব MCQ Standard MCQ অভিন্ন উদ্দীপক MCQ বহুপদী সমাপ্তিসূচক MCQ
কঠিনতা patch({ difficulty: e.target.value })}>সব {DIFFICULTIES.map(d => {d.l} )}
সেভড / আনসেভড patch({ saved: e.target.value })}>সব সেভড আনসেভড
ট্যাগ (একাধিক)
patch({ tag_mode: 'or' })} className={`px-2 py-0.5 rounded font-bold ${f.tag_mode === 'or' ? 'bg-emerald-700 text-white' : 'text-slate-600'}`} title="যেকোনো একটি ট্যাগ থাকলেই">OR
patch({ tag_mode: 'and' })} className={`px-2 py-0.5 rounded font-bold ${f.tag_mode === 'and' ? 'bg-emerald-700 text-white' : 'text-slate-600'}`} title="সব ট্যাগ একসাথে থাকলে">AND
{meta.tags.length === 0 && এখনো কোনো ট্যাগ নেই }
{meta.tags.map(t => { const on = (f.tag_ids || []).includes(t.id); return patch({ tag_ids: on ? f.tag_ids.filter(x => x !== t.id) : [...(f.tag_ids || []), t.id] })} className={`px-2 py-0.5 rounded-full text-[11px] border font-bold ${on ? 'bg-emerald-700 text-white border-emerald-700' : 'bg-slate-50 hover:border-emerald-500'}`}>#{t.name} {toBengaliNumber(t.cnt)} ; })}
)}
{chips.length > 0 && (
সক্রিয় ফিল্টার:
{chips.map(c => {c.label} patch(c.clear)} className="w-4 h-4 rounded-full hover:bg-emerald-200 leading-none" title="এই ফিল্টার সরান">× )}
{(f.tag_ids || []).length > 1 && ({f.tag_mode === 'and' ? 'সব ট্যাগ' : 'যেকোনো ট্যাগ'}) }
সব ফিল্টার মুছুন
)}
{report && (
মোট {toBengaliNumber(report.imported + report.updated + report.skipped)}: যুক্ত {toBengaliNumber(report.imported)}, আপডেট {toBengaliNumber(report.updated)}, duplicate {toBengaliNumber(report.duplicates)}, বাদ {toBengaliNumber(report.skipped)}, failed {toBengaliNumber(report.failed)}
{report.errors.length > 0 &&
{report.errors.map((e, i) => {e} )} }
setReport(null)} className="text-slate-500 font-bold">×
)}
{selIds.length > 0 && (
{toBengaliNumber(selIds.length)}টি নির্বাচিত
{ onInsert(selIds.map(byId).filter(Boolean)); setSelected({}); }} className="px-2.5 py-1 rounded bg-emerald-600 font-bold hover:bg-emerald-500">+ "{activePaperTitle}" এ যুক্ত করুন
{!picker && canEdit && <> setEditIds(selIds)} className="px-2.5 py-1 rounded bg-slate-700 font-bold hover:bg-slate-600">✏️ Bulk Edit setMoveIds(selIds)} className="px-2.5 py-1 rounded bg-slate-700 font-bold hover:bg-slate-600">📂 স্থানান্তর/কপি
কঠিনতা Edit bulkTag('add')} className="px-2.5 py-1 rounded bg-slate-700 font-bold hover:bg-slate-600">+ ট্যাগ bulkTag('remove')} className="px-2.5 py-1 rounded bg-slate-700 font-bold hover:bg-slate-600">− ট্যাগ bulkTag('replace')} className="px-2.5 py-1 rounded bg-slate-700 font-bold hover:bg-slate-600">ট্যাগ বদল
{ setSaved(selIds, 1); setSelected({}); }} className="px-2.5 py-1 rounded bg-slate-700 font-bold hover:bg-slate-600">সেভড করুন
{ setSaved(selIds, 0); setSelected({}); }} className="px-2.5 py-1 rounded bg-slate-700 font-bold hover:bg-slate-600">আনসেভড করুন >}
{!picker && canAdd && !canEdit && setMoveIds(selIds)} className="px-2.5 py-1 rounded bg-blue-700 font-bold">কপি করুন }
{!picker && canDelete && doDelete(selIds, `${toBengaliNumber(selIds.length)}টি`)} className="px-2.5 py-1 rounded bg-rose-600 font-bold hover:bg-rose-500">মুছুন }
setSelected({})} className="ml-auto opacity-70 hover:opacity-100">নির্বাচন বাতিল
)}
{err &&
{err}
}
{data.items.map(entry => (
setSelected(s => ({ ...s, [entry.bankId]: !s[entry.bankId] }))}
expanded={!!expanded[entry.bankId]} onToggle={() => setExpanded(x => ({ ...x, [entry.bankId]: !x[entry.bankId] }))}
onEdit={() => onEdit(entry)} onDuplicate={() => duplicate(entry.bankId)} onMove={() => setMoveIds([entry.bankId])}
onToggleSaved={() => setSaved([entry.bankId], entry.is_saved ? 0 : 1)}
onDelete={() => doDelete([entry.bankId], 'এই প্রশ্নটি')} onInsert={() => onInsert([entry])}
onTagClick={(id) => patch({ tag_ids: (f.tag_ids || []).includes(id) ? f.tag_ids : [...(f.tag_ids || []), id] })} />
))}
{!loading && data.items.length === 0 && !err && (
{chips.length > 0 ? 'এই ফিল্টারে কোনো প্রশ্ন পাওয়া যায়নি।' : 'প্রশ্নব্যাংক এখনো খালি। “প্রশ্ন যোগ করুন” ট্যাব থেকে প্রশ্ন যোগ করুন, অথবা JSON ইমপোর্ট করুন।'}
)}
{data.pages > 1 && (
setFilters(c => ({ ...c, page: c.page - 1 }))} className="px-3 py-1.5 rounded border bg-white disabled:opacity-40 hover:bg-slate-50">‹ আগের
{pageNums[0] > 1 && <> setFilters(c => ({ ...c, page: 1 }))} className="px-3 py-1.5 rounded border bg-white hover:bg-slate-50">{toBengaliNumber(1)} … >}
{pageNums.map(n => setFilters(c => ({ ...c, page: n }))} className={`px-3 py-1.5 rounded border ${n === data.page ? 'bg-emerald-700 text-white border-emerald-700' : 'bg-white hover:bg-slate-50'}`}>{toBengaliNumber(n)} )}
{pageNums[pageNums.length - 1] < data.pages && <>… setFilters(c => ({ ...c, page: data.pages }))} className="px-3 py-1.5 rounded border bg-white hover:bg-slate-50">{toBengaliNumber(data.pages)} >}
= data.pages} onClick={() => setFilters(c => ({ ...c, page: c.page + 1 }))} className="px-3 py-1.5 rounded border bg-white disabled:opacity-40 hover:bg-slate-50">পরের ›
)}
{moveIds &&
setMoveIds(null)} onDone={() => { setMoveIds(null); setSelected({}); refresh(); }} />}
{editIds && setEditIds(null)} onDone={() => { setEditIds(null); setSelected({}); refresh(); }} />}
);
}
// ---------- Class → Subject → Chapter → Topic manager ----------
function TaxColumn({ title, hint, items, selectedId, onSelect, onAdd, onSave, onDelete, withCode, disabledMsg, optional, canAdd, canEdit, canDelete }) {
const [name, setName] = useState(''); const [code, setCode] = useState('');
const [editId, setEditId] = useState(null); const [eName, setEName] = useState(''); const [eCode, setECode] = useState('');
const add = async () => { if (!name.trim()) return; const ok = await onAdd(name.trim(), code.trim()); if (ok) { setName(''); setCode(''); } };
const saveEdit = async (it) => { if (!eName.trim()) return; const ok = await onSave(it, eName.trim(), eCode.trim()); if (ok) setEditId(null); };
return (
{title}{optional && (ঐচ্ছিক) }
{hint}
{disabledMsg ?
{disabledMsg}
: (
<>
{canAdd &&
setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && add()} placeholder="নতুন নাম..." className="flex-1 min-w-[90px] border rounded px-2 py-1 text-sm outline-none focus:border-emerald-500" />
{withCode && setCode(e.target.value)} onKeyDown={e => e.key === 'Enter' && add()} placeholder="কোড (ঐচ্ছিক)" className="w-24 border rounded px-2 py-1 text-sm outline-none focus:border-emerald-500" />}
যোগ
}
{items.length === 0 &&
কিছু নেই
}
{items.map(it => (
))}
>
)}
);
}
function TaxonomyManager({ meta, reloadMeta, showToast, permissions }) {
const ops = { canAdd: permissions.can_add_question_bank, canEdit: permissions.can_edit_question_bank, canDelete: permissions.can_delete_question_bank };
const askConfirm = React.useContext(ConfirmCtx);
const [selC, setSelC] = useState(null); const [selS, setSelS] = useState(null); const [selCh, setSelCh] = useState(null);
const subjects = meta.subjects.filter(s => s.class_id === selC);
const chapters = meta.chapters.filter(c => c.subject_id === selS);
const topics = meta.topics.filter(t => t.chapter_id === selCh);
const call = async (body, after) => {
const res = await bankApi('taxSave', body);
if (res.status !== 'success') { showToast(res.message || 'সংরক্ষণ ব্যর্থ'); return false; }
await reloadMeta(); if (after) after(res.id); return true;
};
const del = (type, it, warn) => askConfirm({ title: `“${it.name}” মুছবেন?`, message: warn, onConfirm: async () => {
const res = await bankApi('taxDelete', { type, id: it.id });
if (res.status !== 'success') { showToast(res.message || 'মুছা যায়নি'); return; }
if (type === 'class' && selC === it.id) { setSelC(null); setSelS(null); setSelCh(null); }
if (type === 'subject' && selS === it.id) { setSelS(null); setSelCh(null); }
if (type === 'chapter' && selCh === it.id) setSelCh(null);
showToast('মুছে ফেলা হয়েছে'); reloadMeta();
} });
return (
প্রথমে শ্রেণি যোগ করুন → শ্রেণি বাছাই করে তার বিষয় (কোড ঐচ্ছিক) → বিষয় বাছাই করে অধ্যায় → অধ্যায় বাছাই করে টপিক (ঐচ্ছিক)। এখানে যা যোগ করবেন, প্রশ্ন যোগ করার ফর্ম ও ফিল্টারে ড্রপডাউন হিসেবে পাবেন। কোনো প্রশ্ন যুক্ত থাকলে সেটি মুছা যাবে না।
{ setSelC(id); setSelS(null); setSelCh(null); }}
onAdd={(n) => call({ type: 'class', name: n }, (id) => { setSelC(id); setSelS(null); setSelCh(null); })} onSave={(it, n) => call({ type: 'class', id: it.id, name: n })}
onDelete={(it) => del('class', it, 'এই শ্রেণির অধীনের সব বিষয়, অধ্যায় ও টপিকও মুছে যাবে।')} />
{ setSelS(id); setSelCh(null); }} onAdd={(n, c) => call({ type: 'subject', parent_id: selC, name: n, code: c }, (id) => { setSelS(id); setSelCh(null); })}
onSave={(it, n, c) => call({ type: 'subject', id: it.id, name: n, code: c })} onDelete={(it) => del('subject', it, 'এই বিষয়ের সব অধ্যায় ও টপিকও মুছে যাবে।')} />
call({ type: 'chapter', parent_id: selS, name: n }, (id) => setSelCh(id))}
onSave={(it, n) => call({ type: 'chapter', id: it.id, name: n })} onDelete={(it) => del('chapter', it, 'এই অধ্যায়ের সব টপিকও মুছে যাবে।')} />
call({ type: 'topic', parent_id: selCh, name: n })} onSave={(it, n) => call({ type: 'topic', id: it.id, name: n })} onDelete={(it) => del('topic', it, 'টপিকটি মুছে ফেলা হবে।')} />
);
}
// ---------- Tag manager ----------
function TagManager({ meta, reloadMeta, showToast, onShowTag, permissions }) {
const askConfirm = React.useContext(ConfirmCtx);
const [name, setName] = useState(''); const [editId, setEditId] = useState(null); const [eName, setEName] = useState('');
const save = async (id, n) => {
const res = await bankApi('tagSave', { id, name: n });
if (res.status !== 'success') { showToast(res.message || 'সংরক্ষণ ব্যর্থ'); return false; }
await reloadMeta(); return true;
};
return (
{permissions.can_add_question_bank &&
setName(e.target.value)} onKeyDown={async e => { if (e.key === 'Enter' && name.trim() && await save(0, name.trim())) setName(''); }} placeholder="নতুন ট্যাগ (যেমন: বোর্ড-প্রশ্ন, গুরুত্বপূর্ণ)..." className="flex-1 border rounded px-3 py-2 text-sm outline-none focus:border-emerald-500" />
{ if (name.trim() && await save(0, name.trim())) setName(''); }} className="px-4 py-2 bg-emerald-700 text-white rounded text-sm font-bold hover:bg-emerald-800">+ ট্যাগ তৈরি
}
{meta.tags.length === 0 &&
এখনো কোনো ট্যাগ নেই
}
{meta.tags.map(t => (
{editId === t.id ? (
<>
setEName(e.target.value)} onKeyDown={async e => { if (e.key === 'Enter' && eName.trim() && await save(t.id, eName.trim())) setEditId(null); }} className="flex-1 border rounded px-2 py-1 text-sm outline-none" />
{ if (eName.trim() && await save(t.id, eName.trim())) setEditId(null); }} className="px-2.5 py-1 text-xs font-bold bg-emerald-700 text-white rounded">সেভ
setEditId(null)} className="px-2.5 py-1 text-xs font-bold border rounded">বাতিল
>
) : (
<>
#{t.name}
{toBengaliNumber(t.cnt)}টি প্রশ্ন
onShowTag(t.id)} className="px-2.5 py-1 text-xs font-bold border rounded hover:bg-slate-50">প্রশ্ন দেখুন
{permissions.can_edit_question_bank && { setEditId(t.id); setEName(t.name); }} className="px-2.5 py-1 text-xs font-bold border rounded hover:bg-slate-50">✎ নাম বদলান }
{permissions.can_delete_question_bank && askConfirm({ title: `#${t.name} মুছবেন?`, message: `ট্যাগটি ${toBengaliNumber(t.cnt)}টি প্রশ্ন থেকে সরে যাবে। প্রশ্নগুলো মুছবে না।`, onConfirm: async () => { const r = await bankApi('tagDelete', { id: t.id }); if (r.status === 'success') { showToast('ট্যাগ মুছে ফেলা হয়েছে'); reloadMeta(); } else showToast(r.message || 'ব্যর্থ'); } })} className="px-2.5 py-1 text-xs font-bold border border-rose-200 text-rose-600 rounded hover:bg-rose-50">✕ }
>
)}
))}
);
}
// ---------- the page itself ----------
function QuestionBankPage({ auth, papers, activePaper, setActivePaperId, onInsert, prefill, clearPrefill, hint, showToast }) {
const [tab, setTab] = useState(prefill && auth.can_add_question_bank ? 'add' : 'list');
const [meta, setMeta] = useState({ classes: [], subjects: [], chapters: [], topics: [], tags: [], codes: [] });
const [metaErr, setMetaErr] = useState('');
const [editing, setEditing] = useState(null);
const [filters, setFilters] = useState(DEFAULT_FILTERS);
const [listVersion, setListVersion] = useState(0);
const bump = useCallback(() => setListVersion(v => v + 1), []);
const reloadMeta = useCallback(async () => {
const d = await bankApi('bankMeta');
if (d && d.status === 'success') { setMeta(d); setMetaErr(''); } else setMetaErr((d && d.message) || 'তথ্য লোড করা যায়নি');
}, []);
useEffect(() => { reloadMeta(); }, []);
useEffect(() => { if (prefill && auth.can_add_question_bank) { setEditing(null); setTab('add'); } }, [prefill, auth.can_add_question_bank]);
const finishAdd = () => { setEditing(null); if (prefill) clearPrefill(); bump(); setTab('list'); };
const canSeeManagers = auth.can_add_question_bank || auth.can_edit_question_bank || auth.can_delete_question_bank;
const tabs = [['list', '📚 প্রশ্ন তালিকা'], ...((auth.can_add_question_bank || (editing && auth.can_edit_question_bank)) ? [['add', editing ? '✏️ প্রশ্ন সম্পাদনা' : '➕ প্রশ্ন যোগ করুন']] : []), ...(canSeeManagers ? [['tax', '🏫 শ্রেণি / বিষয় / অধ্যায়'], ['tags', '🏷 ট্যাগ']] : [])];
return (
{tabs.map(([id, l]) => (
{ if (id === 'add' && tab !== 'add') setEditing(null); setTab(id); }} className={`px-3.5 py-2 rounded-lg text-sm font-bold transition ${tab === id ? 'bg-emerald-700 text-white shadow-sm' : id === 'add' ? 'bg-emerald-50 text-emerald-800 border border-emerald-300 hover:bg-emerald-100' : 'text-slate-600 hover:bg-slate-100'}`}>{l}
))}
যুক্ত হবে:
setActivePaperId(e.target.value)} className="border rounded px-2 py-1.5 bg-white font-bold max-w-[220px] outline-none">
{papers.map(p => {p.title} )}
{metaErr &&
{metaErr}
}
{tab === 'list' && { setEditing(entry); setTab('add'); }} />}
{tab === 'add' && }
{tab === 'tax' && }
{tab === 'tags' && { setFilters({ ...DEFAULT_FILTERS, tag_ids: [id] }); setTab('list'); }} />}
);
}
// ---------- shared meta loader (classes / subjects / chapters / topics / tags) ----------
function useBankMeta() {
const [meta, setMeta] = useState({ classes: [], subjects: [], chapters: [], topics: [], tags: [], codes: [] });
const [metaErr, setMetaErr] = useState('');
const reloadMeta = useCallback(async () => {
const d = await bankApi('bankMeta');
if (d && d.status === 'success') { setMeta(d); setMetaErr(''); } else setMetaErr((d && d.message) || 'তথ্য লোড করা যায়নি');
}, []);
useEffect(() => { reloadMeta(); }, []);
return { meta, metaErr, reloadMeta };
}
// ---------- popup: pick questions from the bank / save a question into the bank ----------
function BankModal({ modal, onClose, activePaper, onInsert, showToast }) {
const { meta, metaErr, reloadMeta } = useBankMeta();
const isPick = modal.mode === 'pick';
const hint = activePaper ? activePaper.header : null;
const [filters, setFilters] = useState(() => ({ ...DEFAULT_FILTERS, q_type: SEC_TO_TYPE[modal.secKey] || '' }));
const [lv, setLv] = useState(0);
const bump = useCallback(() => setLv(v => v + 1), []);
const [added, setAdded] = useState(0);
const seeded = useRef(false);
useEffect(() => {
const h = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h);
}, []);
// pre-select the class / subject of the exam paper once the dropdown data is loaded
useEffect(() => {
if (!isPick || seeded.current || meta.classes.length === 0) return;
seeded.current = true;
const c = meta.classes.find(x => x.name === ((hint && hint.className) || '').trim());
if (!c) return;
const s = meta.subjects.find(x => x.class_id === c.id && x.name === ((hint && hint.subject) || '').trim());
setFilters(f => ({ ...f, class_id: c.id, subject_id: s ? s.id : '' }));
}, [meta]);
const insert = (entries) => { onInsert(entries); setAdded(n => n + entries.length); };
return (
e.stopPropagation()} className="bg-slate-100 rounded-2xl shadow-2xl w-full max-w-6xl h-[94vh] flex flex-col overflow-hidden">
{isPick ? '🗂 প্রশ্নব্যাংক থেকে প্রশ্ন যোগ করুন' : '💾 প্রশ্নব্যাংকে সেভ করুন'}
{isPick ? `ফিল্টার করে প্রশ্ন বেছে “${activePaper ? activePaper.title : ''}” এ যুক্ত করুন${added ? ` · এ পর্যন্ত ${toBengaliNumber(added)}টি যুক্ত হয়েছে` : ''}` : 'শ্রেণি, বিষয় ও অধ্যায় বাছাই করে সেভ করুন — পেজ ছাড়তে হবে না'}
{isPick ? 'সম্পন্ন / বন্ধ করুন' : '✕ বন্ধ'}
{metaErr &&
{metaErr}
}
{isPick
? {}} />
: }
);
}
function App() {
const [auth, setAuth] = useState({ loggedIn: false, role: null, username: null, can_add_question_bank: false, can_edit_question_bank: false, can_delete_question_bank: false, can_manage_question_bank: false });
const [loginForm, setLoginForm] = useState({ username: '', password: '' });
const [papers, setPapers] = useState([]);
const [appView, setAppView] = useState('editor'); // 'editor' | 'bank'
const [bankModal, setBankModal] = useState(null); // {mode:'pick'|'save', secKey, prefill}
const [bankPrefill, setBankPrefill] = useState(null);
const [loading, setLoading] = useState(true);
const [papersLoadError, setPapersLoadError] = useState('');
const [activePaperId, setActivePaperId] = useState(null);
const [activeTab, setActiveTab] = useState('header');
const [leftSidebarView, setLeftSidebarView] = useState('nav'); // 'projects', 'bank', 'settings'
const [rightSidebarTab, setRightSidebarTab] = useState('preview');
const [previewZoom, setPreviewZoom] = useState(0.68);
const [showAnswerKey, setShowAnswerKey] = useState(false);
const [expandedQuestions, setExpandedQuestions] = useState({});
const [activeQ, setActiveQ] = useState(null);
const [navOpen, setNavOpen] = useState({ mcq: true });
const [mobilePane, setMobilePane] = useState('edit');
const [confirmDlg, setConfirmDlg] = useState(null);
const paperImportRef = useRef(null);
const [toast, setToast] = useState(null);
const [saveState, setSaveState] = useState('saved');
const toastTimer = useRef(null);
const askConfirm = useCallback((cfg) => setConfirmDlg(cfg), []);
const showToast = useCallback((msg, undo) => {
clearTimeout(toastTimer.current);
setToast({ msg, undo });
toastTimer.current = setTimeout(() => setToast(null), undo ? 8000 : 2500);
}, []);
const selectQuestion = (secKey, id) => {
setActiveTab(secKey); setActiveQ(id);
setNavOpen(p => ({ ...p, [secKey]: true }));
setExpandedQuestions(p => ({ ...p, [id]: true }));
setMobilePane('edit');
setTimeout(() => document.getElementById('q-' + id)?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 80);
};
useEffect(() => {
const h = (e) => { if (saveState !== 'saved') { e.preventDefault(); e.returnValue = ''; } };
window.addEventListener('beforeunload', h); return () => window.removeEventListener('beforeunload', h);
}, [saveState]);
const toggleAccordion = (id) => {
setExpandedQuestions(prev => ({ ...prev, [id]: !prev[id] }));
};
useEffect(() => {
fetch(API_URL + 'checkAuth').then(res => res.json()).then(data => {
if(data.status === 'success') {
setAuth({ loggedIn: true, role: data.user.role, username: data.user.username, can_add_question_bank: !!data.user.can_add_question_bank, can_edit_question_bank: !!data.user.can_edit_question_bank, can_delete_question_bank: !!data.user.can_delete_question_bank, can_manage_question_bank: !!data.user.can_manage_question_bank });
} else {
setLoading(false);
}
}).catch(() => setLoading(false));
}, []);
useEffect(() => {
if(auth.loggedIn) {
setLoading(true); setPapersLoadError('');
fetch(API_URL + 'getPapers').then(res => res.json()).then((papersData) => {
if (!Array.isArray(papersData)) throw new Error(papersData.message || 'প্রশ্নপত্র লোড করা যায়নি');
if (papersData.length > 0) { setPapers(papersData); setActivePaperId(papersData[0].id); }
else { const fresh = createSampleExamPaper(); setPapers([fresh]); setActivePaperId(fresh.id); }
setLoading(false);
}).catch((e) => { setPapers([]); setPapersLoadError(e.message || 'ড্যাশবোর্ডের তথ্য লোড করা যায়নি'); setLoading(false); });
}
}, [auth.loggedIn]);
useEffect(() => {
if (loading || papersLoadError || papers.length === 0 || !auth.loggedIn) return;
setSaveState('pending');
const timer = setTimeout(() => { setSaveState('saving'); fetch(API_URL + 'savePapers', { method: 'POST', body: JSON.stringify(papers) }).then(r => r.json()).then(d => setSaveState(d.status === 'success' ? 'saved' : 'error')).catch(() => setSaveState('error')); }, 1500);
return () => clearTimeout(timer);
}, [papers, loading, papersLoadError, auth.loggedIn]);
const handleLogin = (e) => {
e.preventDefault();
setPapersLoadError(''); setLoading(true);
fetch(API_URL + 'login', { method: 'POST', body: JSON.stringify(loginForm) })
.then(res => res.json())
.then(data => {
if(data.status === 'success') { setAuth({ loggedIn: true, role: data.role, username: data.username, can_add_question_bank: !!data.can_add_question_bank, can_edit_question_bank: !!data.can_edit_question_bank, can_delete_question_bank: !!data.can_delete_question_bank, can_manage_question_bank: !!data.can_manage_question_bank }); }
else { setLoading(false); alert(data.message || "ভুল ইউজারনেম বা পাসওয়ার্ড!"); }
}).catch(() => { setLoading(false); alert('সার্ভারের সাথে যোগাযোগ করা যায়নি।'); });
};
const handleLogout = () => {
fetch(API_URL + 'logout').then(() => {
setAuth({ loggedIn: false, role: null, username: null, can_add_question_bank: false, can_edit_question_bank: false, can_delete_question_bank: false, can_manage_question_bank: false });
setPapers([]);
setAppView('editor'); setBankPrefill(null);
setLeftSidebarView('nav');
});
};
if (loading) return
;
if (!auth.loggedIn) {
return (
);
}
if (papersLoadError) return
ড্যাশবোর্ড লোড হয়নি {papersLoadError}
window.location.reload()} className="px-4 py-2 rounded bg-emerald-700 text-white font-bold">আবার চেষ্টা করুন লগআউট ;
const activePaper = papers.find((p) => p.id === activePaperId) || papers[0];
const updateActivePaper = (updater) => {
setPapers(prev => prev.map(p => p.id === activePaper.id ? { ...(typeof updater === 'function' ? updater(p) : { ...p, ...updater }), updatedAt: new Date().toISOString() } : p));
};
const updateHeader = (patch) => updateActivePaper((p) => ({ ...p, header: { ...p.header, ...patch } }));
const updateLayout = (patch) => updateActivePaper((p) => ({ ...p, layout: { ...p.layout, ...patch } }));
const updateLabels = (patch) => updateActivePaper((p) => ({ ...p, labels: { ...(p.labels || {}), ...patch } }));
const updateSection = (secKey, patch) => updateActivePaper((p) => ({
...p,
sections: {
...(p.sections||{}),
[secKey]: { ...((p.sections && p.sections[secKey]) || {items:[]}), ...patch }
}
}));
// CRUD Helpers
const addMCQGroup = () => {
const mk = () => ({ id: generateId('mcq'), type: 'standard', text: 'নতুন বহুনির্বাচনী প্রশ্ন লিখুন?', marks: 1, correctIndex: 0, layoutMode: 'auto', statementLayoutMode: 'auto',
options: ['অপশন ক', 'অপশন খ', 'অপশন গ', 'অপশন ঘ'].map(t => ({ id: generateId('opt'), text: t, widthMode: 'auto' })) });
const a = mk(), b = mk();
a.showStimulus = true; a.sharedStimulus = ''; a.stimulusQCount = 2;
updateSection('mcq', { items: [...(activePaper.sections?.mcq?.items || []), a, b] });
selectQuestion('mcq', a.id);
};
// গ্রুপের শেষে আরেকটি MCQ যোগ
const addToGroup = (idx) => {
const items = [...(activePaper.sections?.mcq?.items || [])];
const head = items[idx]; const count = Math.max(1, Number(head.stimulusQCount) || 2);
const nq = { id: generateId('mcq'), type: 'standard', text: 'নতুন বহুনির্বাচনী প্রশ্ন লিখুন?', marks: 1, correctIndex: 0, layoutMode: 'auto', statementLayoutMode: 'auto',
options: ['অপশন ক', 'অপশন খ', 'অপশন গ', 'অপশন ঘ'].map(t => ({ id: generateId('opt'), text: t, widthMode: 'auto' })) };
items[idx] = { ...head, stimulusQCount: count + 1 };
items.splice(idx + count, 0, nq);
updateSection('mcq', { items });
selectQuestion('mcq', nq.id);
};
const addMCQ = (type = 'standard') => {
let defaultOptions = [
{ id: 'o1', text: 'অপশন ক', widthMode: 'auto' },
{ id: 'o2', text: 'অপশন খ', widthMode: 'auto' },
{ id: 'o3', text: 'অপশন গ', widthMode: 'auto' },
{ id: 'o4', text: 'অপশন ঘ', widthMode: 'auto' }
];
if (type === 'statement') {
defaultOptions = [
{ id: 'o1', text: 'i ও ii', widthMode: 'auto' },
{ id: 'o2', text: 'i ও iii', widthMode: 'auto' },
{ id: 'o3', text: 'ii ও iii', widthMode: 'auto' },
{ id: 'o4', text: 'i, ii ও iii', widthMode: 'auto' }
];
}
const newItem = {
id: generateId('mcq'),
type,
text: type === 'statement' ? 'উদ্দীপকের তথ্য অনুযায়ী বৈশিষ্ট্য হলো—' : 'নতুন বহুনির্বাচনী প্রশ্ন লিখুন?',
statements: type === 'statement' ? ['প্রথম বিবৃতি লিখুন', 'দ্বিতীয় বিবৃতি লিখুন', 'তৃতীয় বিবৃতি লিখুন'] : undefined,
statementLayoutMode: 'auto',
subPrompt: type === 'statement' ? 'নিচের কোনটি সঠিক?' : undefined,
marks: 1,
correctIndex: 0,
layoutMode: 'auto',
options: defaultOptions
};
updateSection('mcq', { items: [...(activePaper.sections?.mcq?.items || []), newItem] });
selectQuestion('mcq', newItem.id);
};
const addCreativeQuestion = () => {
const newCQ = { id: generateId('cq'), stimulus: 'নতুন উদ্দীপক লিখুন...', image: null, subQuestions: [ { label: 'ক', text: 'জ্ঞানমূলক?', marks: 1 }, { label: 'খ', text: 'অনুধাবনমূলক?', marks: 2 }, { label: 'গ', text: 'প্রয়োগমূলক?', marks: 3 }, { label: 'ঘ', text: 'উচ্চতর দক্ষতামূলক?', marks: 4 } ] };
updateSection('creative', { items: [...(activePaper.sections?.creative?.items || []), newCQ] });
selectQuestion('creative', newCQ.id);
};
const addShortQuestion = () => {
const newSQ = { id: generateId('sq'), text: 'নতুন সংক্ষিপ্ত প্রশ্ন লিখুন।', marks: 2, showIndividualMarks: false };
updateSection('short', { items: [...(activePaper.sections?.short?.items || []), newSQ] });
selectQuestion('short', newSQ.id);
};
const moveQuestion = (secKey, idx, dir) => {
const list = [...(activePaper.sections?.[secKey]?.items || [])];
const [start, end] = secKey === 'mcq' ? questionBlockBounds(list, idx) : [idx, idx + 1];
if ((dir < 0 && start === 0) || (dir > 0 && end >= list.length)) return;
const block = list.splice(start, end - start);
list.splice(dir < 0 ? start - 1 : start + 1, 0, ...block);
updateSection(secKey, { items: list });
};
const reorderQuestion = (secKey, fromIdx, toIdx) => {
if (fromIdx === toIdx) return;
const list = [...(activePaper.sections?.[secKey]?.items || [])];
const [start, end] = secKey === 'mcq' ? questionBlockBounds(list, fromIdx) : [fromIdx, fromIdx + 1];
if (toIdx >= start && toIdx < end) return;
const block = list.splice(start, end - start);
list.splice(toIdx, 0, ...block);
updateSection(secKey, { items: list });
};
const duplicateQuestion = (secKey, idx) => {
const list = [...(activePaper.sections?.[secKey]?.items || [])];
if(list.length === 0) return;
const [start, end] = secKey === 'mcq' ? questionBlockBounds(list, idx) : [idx, idx + 1];
const copies = list.slice(start, end).map(item => ({ ...JSON.parse(JSON.stringify(item)), id: generateId(secKey) }));
list.splice(end, 0, ...copies); setActiveQ(copies[0].id); showToast('প্রশ্ন ডুপ্লিকেট হয়েছে');
updateSection(secKey, { items: list });
};
const SEC_NAMES = { mcq: 'বহুনির্বাচনী', creative: 'সৃজনশীল', short: 'সংক্ষিপ্ত' };
const deleteQuestion = (secKey, idx) => {
const originalItems = [...(activePaper.sections?.[secKey]?.items || [])];
const item = originalItems[idx];
if (!item) return;
askConfirm({
title: 'প্রশ্ন মুছে ফেলবেন?',
message: `${SEC_NAMES[secKey]} প্রশ্ন ${toBengaliNumber(idx + 1)}: "${getQuestionPreview(item)}" মুছে ফেলা হবে।`,
onConfirm: () => {
const nextItems = [...originalItems];
if (secKey === 'mcq') {
const [start, end] = questionBlockBounds(nextItems, idx);
if (end - start > 1) {
const count = end - start - 1;
if (idx === start) nextItems[start + 1] = { ...nextItems[start + 1], sharedStimulus: item.sharedStimulus, stimulusImage: item.stimulusImage || null, stimulusInstruction: item.stimulusInstruction || '', stimulusQCount: count, showStimulus: true };
else nextItems[start] = { ...nextItems[start], stimulusQCount: count };
}
}
nextItems.splice(idx, 1);
updateActivePaper(p => ({ ...p, sections: { ...p.sections, [secKey]: { ...p.sections[secKey], items: nextItems } } }));
if (activeQ === item.id) setActiveQ(null);
showToast('প্রশ্নটি মুছে ফেলা হয়েছে', () => {
updateActivePaper(p => ({ ...p, sections: { ...p.sections, [secKey]: { ...p.sections[secKey], items: originalItems } } }));
setToast(null);
});
}
});
};
const deletePaper = (p) => askConfirm({
title: 'প্রশ্নপত্র মুছে ফেলবেন?', message: `"${p.title}" সম্পূর্ণ মুছে যাবে।`,
onConfirm: () => {
const next = papers.filter(x => x.id !== p.id);
if (next.length === 0) { const f = createSampleExamPaper(); f.title = nextPaperTitle([]); setPapers([f]); setActivePaperId(f.id); }
else { setPapers(next); if (p.id === activePaperId) setActivePaperId(next[0].id); }
showToast('প্রশ্নপত্র মুছে ফেলা হয়েছে', () => { setPapers(cur => [p, ...cur]); setActivePaperId(p.id); setToast(null); });
}
});
// 💾 button inside the exam editor -> opens the Question Bank "add" tab pre-filled (so it gets classified)
const saveToQuestionBank = (secKey, item) => {
if (secKey === 'mcq' && item.sharedStimulus && plainText(item.sharedStimulus)) {
// অভিন্ন উদ্দীপকের গ্রুপ: পুরো গ্রুপ একসাথে একটি গ্রুপ-প্রশ্ন হিসেবে সেভ হবে
const list = activePaper.sections?.mcq?.items || [];
const start = list.findIndex(q => q.id === item.id);
const count = Math.max(1, Number(item.stimulusQCount) || 2);
const members = list.slice(start, start + count).map(q => {
const m = JSON.parse(JSON.stringify(q));
delete m.showStimulus; delete m.sharedStimulus; delete m.stimulusImage; delete m.stimulusQCount; delete m.stimulusInstruction;
return m;
});
const group = { isGroup: true, sharedStimulus: item.sharedStimulus, stimulusImage: item.stimulusImage || null, stimulusInstruction: item.stimulusInstruction || '', questions: members };
setBankModal({ mode: 'save', secKey, prefill: { secKey, item: group, nonce: Date.now() } });
return;
}
const copy = JSON.parse(JSON.stringify(item)); delete copy.id;
setBankModal({ mode: 'save', secKey, prefill: { secKey, item: copy, nonce: Date.now() } });
};
// Insert one or more bank entries into the active exam paper (MCQ groups keep their single shared stimulus)
const insertBankEntries = (entries) => {
if (!activePaper || !Array.isArray(entries) || entries.length === 0) return;
const add = { mcq: [], creative: [], short: [] };
let invalidGroups = 0;
entries.forEach((e) => {
if (!e || !e.item || typeof e.item !== 'object' || Array.isArray(e.item)) return;
const it = JSON.parse(JSON.stringify(e.item));
const sourceMeta = { bankId: e.bankId, subject: e.subject, chapter: e.chapter, tags: e.tags || [] };
if (e.q_type === 'MCQ' && it.isGroup) {
const qs = Array.isArray(it.questions) ? it.questions.filter(q => q && typeof q === 'object' && !Array.isArray(q)) : [];
if (!qs.length) { invalidGroups++; return; }
qs.forEach((q, i) => {
const c = { ...q, options: Array.isArray(q.options) ? q.options : [], id: generateId('mcq'), sourceMeta };
if (i === 0) { c.showStimulus = true; c.sharedStimulus = typeof it.sharedStimulus === 'string' ? it.sharedStimulus : ''; c.stimulusImage = typeof it.stimulusImage === 'string' ? it.stimulusImage : null; c.stimulusQCount = qs.length; if (typeof it.stimulusInstruction === 'string' && it.stimulusInstruction) c.stimulusInstruction = it.stimulusInstruction; }
add.mcq.push(c);
});
} else if (e.q_type === 'MCQ') { it.options = Array.isArray(it.options) ? it.options : []; it.id = generateId('mcq'); it.sourceMeta = sourceMeta; add.mcq.push(it); }
else if (e.q_type === 'Creative') { it.id = generateId('cq'); it.sourceMeta = sourceMeta; add.creative.push(it); }
else { add.short.push({ id: generateId('sq'), text: it.text || '', image: it.image || null, marks: Number(e.marks) || Number(it.marks) || 2, showIndividualMarks: false, sourceMeta }); }
});
const total = add.mcq.length + add.creative.length + add.short.length;
if (!total) { showToast(invalidGroups ? 'এই অভিন্ন উদ্দীপকের প্রশ্নের তথ্য সঠিক নয়। প্রশ্নব্যাংকের প্রশ্নটি সম্পাদনা করে আবার চেষ্টা করুন।' : 'নির্বাচিত প্রশ্ন যোগ করা যায়নি।'); return; }
updateActivePaper((p) => {
const sections = { ...(p.sections || {}) };
['mcq', 'creative', 'short'].forEach((k) => {
if (add[k].length) sections[k] = { ...(sections[k] || { items: [] }), enabled: true, items: [...((sections[k] && sections[k].items) || []), ...add[k]] };
});
return { ...p, sections };
});
showToast(`${toBengaliNumber(total)}টি প্রশ্ন “${activePaper.title}” এ যুক্ত হয়েছে${invalidGroups ? ` · ${toBengaliNumber(invalidGroups)}টি ভুল গ্রুপ বাদ গেছে` : ''}`);
};
const handleDownloadDirectPDF = async () => {
const printContainer = document.getElementById('pdf-capture-stage');
if (!printContainer) return;
const sheetNodes = Array.from(printContainer.querySelectorAll('.a4-paper-sheet'));
if (sheetNodes.length === 0) return;
try {
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
for (let i = 0; i < sheetNodes.length; i++) {
sheetNodes[i].style.width = '210mm'; sheetNodes[i].style.height = '297mm'; sheetNodes[i].style.transform = 'scale(1)';
// Prevent text cutoff by capturing full content height natively and fixing scroll issues
const canvas = await window.html2canvas(sheetNodes[i], { scale: 2, useCORS: true, backgroundColor: '#ffffff', windowWidth: 794, scrollY: -window.scrollY });
const imgData = canvas.toDataURL('image/jpeg', 1.0);
if (i > 0) pdf.addPage('a4', 'portrait');
pdf.addImage(imgData, 'JPEG', 0, 0, 210, 297, undefined, 'FAST');
}
pdf.save(`${activePaper.title}.pdf`);
} catch (err) { alert("PDF তৈরিতে সমস্যা, প্রিন্ট ব্যবহার করুন।"); window.print(); }
};
const handleDownloadDocx = () => {
const printContainer = document.getElementById('pdf-capture-stage');
if (!printContainer) return;
const css = ``;
const header = `
Export ${css}`;
const footer = "";
const sourceHTML = header + printContainer.innerHTML + footer;
const blob = new Blob(['', sourceHTML], { type: 'application/msword' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a'); link.href = url; link.download = `${activePaper.title}.doc`;
document.body.appendChild(link); link.click(); document.body.removeChild(link);
setTimeout(() => URL.revokeObjectURL(url), 1000);
};
const exportExamPaper = (paper) => {
const payload = { format: 'qmaker-exam-paper', version: 1, exported_at: new Date().toISOString(), paper };
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${String(paper.title || 'exam-paper').replace(/[^\p{L}\p{N}-]+/gu, '-').replace(/^-|-$/g, '') || 'exam-paper'}.json`;
document.body.appendChild(link); link.click(); document.body.removeChild(link);
setTimeout(() => URL.revokeObjectURL(url), 1000);
showToast(`“${paper.title || 'প্রশ্নপত্র'}” JSON export হয়েছে`);
};
const importExamPaper = async (event) => {
const file = event.target.files?.[0]; event.target.value = '';
if (!file) return;
let decoded;
try { decoded = JSON.parse(await file.text()); }
catch (e) { showToast('ফাইলটি সঠিক JSON নয়'); return; }
const paper = decoded?.format === 'qmaker-exam-paper' ? decoded.paper : decoded;
if (!paper || typeof paper !== 'object' || !paper.header || typeof paper.header !== 'object' || !paper.sections || typeof paper.sections !== 'object') {
showToast('এটি বৈধ প্রশ্নপত্র JSON নয়'); return;
}
for (const key of ['mcq', 'creative', 'short']) {
const section = paper.sections[key];
if (!section || typeof section !== 'object' || !Array.isArray(section.items)) {
showToast(`প্রশ্নপত্রের “${key}” বিভাগে items তালিকা নেই`); return;
}
}
const imported = cloneJson(paper);
imported.id = generateId('paper');
imported.title = String(imported.title || nextPaperTitle(papers));
const counts = ['mcq', 'creative', 'short'].map(key => `${key.toUpperCase()} ${toBengaliNumber(imported.sections[key]?.items?.length || 0)}`).join(' · ');
askConfirm({
title: 'প্রশ্নপত্র Import করবেন?', confirmText: 'নতুন প্রশ্নপত্র হিসেবে যোগ করুন', icon: '📥',
message: `“${imported.title}” import হবে। ${counts}. বর্তমান প্রশ্নপত্রগুলো অক্ষত থাকবে।`,
onConfirm: () => {
setPapers(current => [imported, ...current]); setActivePaperId(imported.id);
setAppView('editor'); setLeftSidebarView('projects'); showToast('প্রশ্নপত্র import হয়েছে');
},
});
};
const getQuestionPreview = (item) => {
const plainText = (item.text || item.stimulus || '').replace(/<[^>]*>/g, '').trim();
return plainText ? plainText.substring(0, 35) + (plainText.length > 35 ? '...' : '') : 'খালি প্রশ্ন...';
};
return (
ড্যাশবোর্ড - স্বাগতম, {auth.username} {auth.role.toUpperCase()}
setAppView('editor')} className={`px-3 py-1.5 rounded-md whitespace-nowrap ${appView === 'editor' ? 'bg-emerald-700 text-white' : 'text-slate-600 hover:bg-slate-200'}`}>📝 প্রশ্নপত্র
setAppView('bank')} className={`px-3 py-1.5 rounded-md whitespace-nowrap ${appView === 'bank' ? 'bg-emerald-700 text-white' : 'text-slate-600 hover:bg-slate-200'}`}>🗂 প্রশ্নব্যাংক
{saveState === 'saved' ? '✓ সংরক্ষিত' : saveState === 'error' ? '⚠ সেভ ব্যর্থ' : '● সংরক্ষণ হচ্ছে...'}
{appView === 'editor' &&
setBankModal({ mode: 'pick', secKey: activeTab })} className="px-3 py-1.5 border border-blue-300 bg-blue-50 text-blue-800 rounded text-xs font-bold whitespace-nowrap">🗂 ব্যাংক থেকে যোগ }
{appView === 'editor' &&
setShowAnswerKey(!showAnswerKey)} className={`px-3 py-1.5 border rounded text-xs font-bold ${showAnswerKey ? 'bg-emerald-50 border-emerald-500' : 'bg-white'}`}>উত্তরপত্র }
{appView === 'editor' &&
exportExamPaper(activePaper)} className="px-3 py-1.5 bg-blue-700 text-white rounded text-xs font-bold whitespace-nowrap">📄 প্রশ্নপত্র JSON }
{appView === 'editor' &&
DOC Export }
{appView === 'editor' &&
⬇️ PDF / প্রিন্ট }
লগআউট
{appView === 'bank' && (
setBankPrefill(null)} hint={activePaper ? activePaper.header : null} showToast={showToast} />
)}
{appView === 'editor' && (<>
{[['nav', 'প্রশ্ন তালিকা'], ['edit', 'এডিটর'], ['preview', 'প্রিভিউ']].map(([id, l]) => setMobilePane(id)} className={`py-2 border-b-2 ${mobilePane === id ? 'border-emerald-700 text-emerald-800 bg-emerald-50' : 'border-transparent text-slate-600'}`}>{l} )}
{/* 1. LEFT SIDEBAR */}
setLeftSidebarView('nav')} className={`py-2.5 border-b-2 transition ${leftSidebarView === 'nav' ? 'border-emerald-700 text-emerald-800 bg-slate-50' : 'border-transparent hover:bg-slate-50'}`}>প্রশ্ন
setLeftSidebarView('projects')} className={`py-2.5 border-b-2 transition ${leftSidebarView === 'projects' ? 'border-emerald-700 text-emerald-800 bg-slate-50' : 'border-transparent hover:bg-slate-50'}`}>ফাইল
setLeftSidebarView('bank')} className={`py-2.5 border-b-2 transition ${leftSidebarView === 'bank' ? 'border-emerald-700 text-emerald-800 bg-slate-50' : 'border-transparent hover:bg-slate-50'}`}>প্রশ্নব্যাংক
{leftSidebarView === 'nav' && (
updateSection(k, { enabled: v })}
onReorder={reorderQuestion} onPick={(k) => { setActiveTab(k); setBankModal({ mode: 'pick', secKey: k }); }}
onAdd={(k) => k === 'mcq' ? addMCQ('standard') : k === 'creative' ? addCreativeQuestion() : k === 'short' ? addShortQuestion() : (() => { const sec = activePaper.sections[k]; const item = { id: generateId('custom'), text: '', marks: 2 }; updateSection(k, { enabled: true, items: [...(sec.items || []), item] }); selectQuestion(k, item.id); })()} />
)}
{leftSidebarView === 'projects' && (
{ const newP = createSampleExamPaper(); newP.title = nextPaperTitle(papers); setPapers([newP, ...papers]); setActivePaperId(newP.id); }} className="w-full bg-emerald-700 text-white text-xs py-2 rounded mb-3 shadow-sm hover:bg-emerald-800">+ নতুন প্রশ্নপত্র
{papers.map(p => (
setActivePaperId(p.id)} className="w-full text-left font-bold text-xs">{p.title}
exportExamPaper(p)} className="text-blue-700 hover:text-blue-900 font-bold">⬇ JSON Export
{ const copy = JSON.parse(JSON.stringify(p)); copy.id = generateId('paper'); copy.title = nextPaperTitle(papers); setPapers([copy, ...papers]); setActivePaperId(copy.id); }} className="text-slate-600 hover:text-emerald-700 font-bold">ডুপ্লিকেট
deletePaper(p)} className="text-rose-600 font-bold">মুছুন
))}
)}
{leftSidebarView === 'bank' && (
🗂
প্রশ্নব্যাংক এখন আলাদা পেজে — সেখানে ফিল্টার, সার্চ, ট্যাগ, MCQ গ্রুপ, JSON ইমপোর্ট/এক্সপোর্ট সবকিছু আছে।
setAppView('bank')} className="w-full bg-emerald-700 text-white text-xs font-bold py-2.5 rounded hover:bg-emerald-800">প্রশ্নব্যাংক খুলুন
)}
{ setLeftSidebarView('settings'); setMobilePane('edit'); }} className={`w-full text-xs py-2 rounded font-bold transition shadow-sm border ${leftSidebarView === 'settings' ? 'bg-slate-800 text-white border-slate-800' : 'bg-white text-slate-700 border-slate-300 hover:bg-slate-200'}`}>⚙️ সেটিংস ও ইউজার
{leftSidebarView === 'settings' ? (
) : (
<>
{/* 2. CENTER MAIN EDITOR */}
{[{id: 'header', label: 'শিরোনাম'}, ...sectionOrderFor(activePaper).map(id => ({ id, label: `${activePaper.sections?.[id]?.title || SECTION_TITLES[id] || 'কাস্টম বিভাগ'} (${activePaper.sections?.[id]?.items?.length||0})` }))].map(t => (
setActiveTab(t.id)} className={`flex-1 py-1.5 rounded-lg text-[11px] sm:text-xs font-bold transition ${activeTab === t.id ? 'bg-emerald-700 text-white shadow-sm' : 'text-slate-600 hover:bg-slate-100'}`}>{t.label}
))}
{
const n = sectionOrderFor(activePaper).filter(k => !FIXED_SECTION_ORDER.includes(k)).length + 1;
const key = `custom_${Date.now()}`;
updateActivePaper(p => ({ ...p, sections: { ...(p.sections || {}), [key]: { enabled: true, title: `নতুন বিভাগ ${n}`, marksText: '', instruction: '', columns: 2, pageBreakBefore: false, items: [] } }, sectionOrder: [...sectionOrderFor(p), key] }));
setActiveTab(key); setNavOpen(p => ({ ...p, [key]: true }));
}} className="px-3 py-2 rounded-lg text-xs font-bold border border-dashed border-emerald-500 text-emerald-700 bg-white hover:bg-emerald-50">+ কাস্টম বিভাগ
{sectionOrderFor(activePaper).map((key, i, order) => {activePaper.sections[key]?.title || SECTION_TITLES[key] || 'কাস্টম বিভাগ'}{i > 0 && updateActivePaper(p => { const a = sectionOrderFor(p); [a[i-1], a[i]] = [a[i], a[i-1]]; return { ...p, sectionOrder: a }; })}>↑ }{i < order.length - 1 && updateActivePaper(p => { const a = sectionOrderFor(p); [a[i], a[i+1]] = [a[i+1], a[i]]; return { ...p, sectionOrder: a }; })}>↓ } )}
{activeTab === 'header' && (
)}
{activeTab === 'mcq' && (
{(activePaper.sections?.mcq?.items || []).map((item, idx) => {
const isExpanded = expandedQuestions[item.id] !== false;
return (
setActiveQ(item.id)} className={`bg-white rounded-xl border p-4 space-y-3 shadow-sm scroll-mt-20 transition ${activeQ === item.id ? 'ring-2 ring-emerald-500 border-emerald-400' : ''}`}>
toggleAccordion(item.id)} className="font-bold text-slate-500 hover:text-slate-800 w-6 h-6 flex items-center justify-center bg-slate-100 rounded">{isExpanded ? '▼' : '▶'}
{toBengaliNumber(idx+1)}. {getQuestionPreview(item)}
{
const type = e.target.value;
let newOptions = item.options;
// যদি সাধারণ থেকে বহুপদীতে পরিবর্তন করা হয়, তবে অপশনগুলো অটো চেঞ্জ হবে
if(type === 'statement' && item.type !== 'statement') {
newOptions = [
{ id: generateId('opt'), text: 'i ও ii', widthMode: 'auto' },
{ id: generateId('opt'), text: 'i ও iii', widthMode: 'auto' },
{ id: generateId('opt'), text: 'ii ও iii', widthMode: 'auto' },
{ id: generateId('opt'), text: 'i, ii ও iii', widthMode: 'auto' }
];
}
updateSection('mcq', { items: activePaper.sections.mcq.items.map((q,i) => i===idx ? {...item, type, options: newOptions, statements: type==='statement'?(item.statements&&item.statements.length?item.statements:['প্রথম বিবৃতি লিখুন','দ্বিতীয় বিবৃতি লিখুন','তৃতীয় বিবৃতি লিখুন']):undefined} : q) });
}} className="border rounded px-1 font-bold outline-none bg-slate-50">
সাধারণ
বহুপদী
duplicateQuestion('mcq', idx)} className="bg-slate-100 px-1.5 py-1 rounded hover:bg-slate-200" title="কপি করুন">📋
moveQuestion('mcq', idx, -1)} className="bg-slate-100 px-1.5 py-1 rounded hover:bg-slate-200">↑
moveQuestion('mcq', idx, 1)} className="bg-slate-100 px-1.5 py-1 rounded hover:bg-slate-200">↓
saveToQuestionBank('mcq', item)} className="bg-blue-50 text-blue-600 px-1.5 py-1 rounded hover:bg-blue-100" title="ব্যাংকে সেভ করুন">💾
deleteQuestion('mcq', idx)} className="bg-rose-50 text-rose-600 px-1.5 py-1 rounded hover:bg-rose-100" title="মুছুন">✕
{isExpanded && (
<>
{/* New Shared Stimulus Block */}
{item.showStimulus || (item.sharedStimulus && item.sharedStimulus.trim() !== '') ? (
(() => {
const groupNumbers = stimulusQuestionNumbers(activePaper.sections.mcq.items, idx);
const count = groupNumbers.length;
const startNum = groupNumbers[0];
const endNum = groupNumbers[groupNumbers.length - 1];
const defaultInst = getStimulusInstruction(item, activePaper.sections.mcq.items, idx);
const groupLabel = groupNumbers.length > 2 && groupNumbers.every((number, i) => i === 0 || number === groupNumbers[i - 1] + 1)
? `${toBengaliNumber(startNum)}-${toBengaliNumber(endNum)}`
: groupNumbers.length === 2
? `${toBengaliNumber(groupNumbers[0])} ও ${toBengaliNumber(groupNumbers[1])}`
: `${groupNumbers.slice(0, -1).map(toBengaliNumber).join(', ')} ও ${toBengaliNumber(groupNumbers[groupNumbers.length - 1])}`;
return (
📄 অভিন্ন উদ্দীপক — প্রশ্ন {groupLabel}
moveQuestion('mcq', idx, -1)} disabled={idx === 0} className="w-7 h-7 flex items-center justify-center rounded border border-amber-200 text-amber-700 hover:bg-amber-100 bg-white disabled:opacity-40 font-bold">↑
moveQuestion('mcq', idx, 1)} disabled={idx === activePaper.sections.mcq.items.length - 1} className="w-7 h-7 flex items-center justify-center rounded border border-amber-200 text-amber-700 hover:bg-amber-100 bg-white disabled:opacity-40 font-bold">↓
updateSection('mcq', { items: activePaper.sections.mcq.items.map((q,i) => i===idx ? {...item, showStimulus: false, sharedStimulus: '', stimulusInstruction: '', stimulusQCount: 2, stimulusImage: null} : q) })} className="w-7 h-7 flex items-center justify-center rounded border border-rose-200 text-rose-500 hover:bg-rose-50 bg-white ml-1 font-bold">✕
updateSection('mcq', { items: activePaper.sections.mcq.items.map((q,i) => i===idx ? {...item, sharedStimulus: val} : q) })} placeholder="উদ্দীপকের লেখা এখানে লিখুন..." minHeight="60px" />
updateSection('mcq', { items: activePaper.sections.mcq.items.map((q,i) => i===idx ? {...item, stimulusImage: img} : q) })} />
);
})()
) : (
updateSection('mcq', { items: activePaper.sections.mcq.items.map((q,i) => i===idx ? {...item, showStimulus: true} : q) })} className="w-full text-left text-[11px] text-emerald-700 font-bold border border-dashed border-emerald-300 rounded px-3 py-1.5 mb-3 bg-emerald-50/50 hover:bg-emerald-100 transition">+ প্রশ্নের আগে অভিন্ন উদ্দীপক (Shared Stimulus) যোগ করুন
)}
updateSection('mcq', { items: activePaper.sections.mcq.items.map((q,i) => i===idx ? {...item, text: val} : q) })} />
{item.type === 'statement' && (
)}
updateSection('mcq', { items: activePaper.sections.mcq.items.map((q,i) => i===idx ? {...item, image: img} : q) })} />
updateSection('mcq', { items: activePaper.sections.mcq.items.map((q,i) => i===idx ? updated : q) })} />
>
)}
);
})}
{ setActiveTab('mcq'); setBankModal({ mode: 'pick', secKey: 'mcq' }); }} className="flex-1 bg-blue-700 text-white text-xs py-2.5 px-3 rounded-lg font-bold hover:bg-blue-800 shadow-sm">🗂 ব্যাংক থেকে MCQ যোগ
addMCQ('standard')} className="flex-1 bg-emerald-700 text-white text-xs py-2.5 px-3 rounded-lg font-bold hover:bg-emerald-800 shadow-sm">+ নতুন সাধারণ প্রশ্ন
addMCQ('statement')} className="flex-1 bg-slate-800 text-white text-xs py-2.5 px-3 rounded-lg font-bold hover:bg-slate-700 shadow-sm">+ নতুন বহুপদী প্রশ্ন
+ MCQ গ্রুপ (অভিন্ন উদ্দীপক)
)}
{activeTab === 'creative' && (
{(activePaper.sections?.creative?.items || []).map((item, idx) => {
const isExpanded = expandedQuestions[item.id] !== false;
return (
setActiveQ(item.id)} className={`bg-white rounded-2xl border-2 p-4 shadow-sm scroll-mt-20 transition ${activeQ === item.id ? 'border-emerald-500' : 'border-slate-200'}`}>
toggleAccordion(item.id)} className="font-bold text-emerald-800 hover:text-emerald-950 w-8 h-8 flex items-center justify-center bg-emerald-100 rounded-md border border-emerald-200 shadow-sm">{isExpanded ? '▼' : '▶'}
সৃজনশীল {toBengaliNumber(idx+1)}. {getQuestionPreview(item)}
duplicateQuestion('creative', idx)} className="bg-white border border-slate-200 text-slate-600 w-8 h-8 flex items-center justify-center rounded-md hover:bg-slate-50 shadow-sm" title="কপি করুন">📋
moveQuestion('creative', idx, -1)} disabled={idx === 0} className="bg-white border border-slate-200 text-slate-600 w-8 h-8 flex items-center justify-center rounded-md hover:bg-slate-50 shadow-sm disabled:opacity-40 font-bold" title="উপরে">↑
moveQuestion('creative', idx, 1)} disabled={idx === activePaper.sections.creative.items.length - 1} className="bg-white border border-slate-200 text-slate-600 w-8 h-8 flex items-center justify-center rounded-md hover:bg-slate-50 shadow-sm disabled:opacity-40 font-bold" title="নিচে">↓
saveToQuestionBank('creative', item)} className="bg-white border border-slate-200 text-slate-600 w-8 h-8 flex items-center justify-center rounded-md hover:bg-slate-50 shadow-sm" title="সেভ করুন">💾
deleteQuestion('creative', idx)} className="bg-white border border-rose-200 text-rose-500 w-8 h-8 flex items-center justify-center rounded-md hover:bg-rose-50 shadow-sm font-bold" title="মুছুন">✕
{isExpanded && (
updateSection('creative', { items: activePaper.sections.creative.items.map((q,i) => i===idx ? {...item, stimulus: val} : q) })} placeholder="উদ্দীপক..." minHeight="60px" />
updateSection('creative', { items: activePaper.sections.creative.items.map((q,i) => i===idx ? {...item, image: img} : q) })} />
উপ-প্রশ্নসমূহ (ক, খ, গ, ঘ)
{(item.subQuestions||[]).map((sq, sIdx) => (
))}
)}
);
})}
{ setActiveTab('creative'); setBankModal({ mode: 'pick', secKey: 'creative' }); }} className="bg-blue-700 text-white text-sm py-3 px-4 rounded-xl font-bold w-full hover:bg-blue-800 shadow-sm">🗂 ব্যাংক থেকে সৃজনশীল প্রশ্ন যোগ
+ নতুন সৃজনশীল প্রশ্ন যুক্ত করুন
)}
{activeTab === 'short' && (
{(activePaper.sections?.short?.items || []).map((item, idx) => (
setActiveQ(item.id)} className={`bg-white rounded-xl border p-3 flex justify-between gap-3 shadow-sm scroll-mt-20 transition ${activeQ === item.id ? 'ring-2 ring-emerald-500 border-emerald-400' : ''}`}>
প্রশ্ন {toBengaliNumber(idx+1)}
updateSection('short', { items: activePaper.sections.short.items.map((q,i) => i===idx ? {...item, text: val} : q) })} compact />
duplicateQuestion('short', idx)} className="bg-slate-100 px-2 py-1.5 rounded hover:bg-slate-200">📋
saveToQuestionBank('short', item)} className="bg-blue-50 text-blue-600 px-2 py-1.5 rounded hover:bg-blue-100">💾
deleteQuestion('short', idx)} className="bg-rose-50 text-rose-600 px-2 py-1.5 rounded hover:bg-rose-100">✕
))}
{ setActiveTab('short'); setBankModal({ mode: 'pick', secKey: 'short' }); }} className="bg-blue-700 text-white text-sm py-3 px-4 rounded-xl font-bold w-full hover:bg-blue-800 shadow-sm">🗂 ব্যাংক থেকে সংক্ষিপ্ত প্রশ্ন যোগ
+ নতুন সংক্ষিপ্ত প্রশ্ন যুক্ত করুন
)}
{activePaper.sections?.[activeTab] && !FIXED_SECTION_ORDER.includes(activeTab) && (() => {
const sec = activePaper.sections[activeTab];
const items = sec.items || [];
const setItems = (next) => updateSection(activeTab, { items: next });
return
{items.map((item, idx) =>
প্রশ্ন {toBengaliNumber(idx+1)}
setItems(items.map((q,i) => i === idx ? { ...q, text: val } : q))} compact /> { const a=[...items]; [a[idx-1],a[idx]]=[a[idx],a[idx-1]]; setItems(a); }} className="px-2 py-1 bg-slate-100 rounded disabled:opacity-40">↑ { const a=[...items]; [a[idx],a[idx+1]]=[a[idx+1],a[idx]]; setItems(a); }} className="px-2 py-1 bg-slate-100 rounded disabled:opacity-40">↓ setItems(items.filter((_,i)=>i!==idx))} className="px-2 py-1 bg-rose-50 text-rose-600 rounded">✕
)}
setItems([...items, { id: generateId('custom'), text: '', marks: 2 }])} className="w-full bg-emerald-700 text-white text-sm py-3 rounded-xl font-bold">+ নতুন প্রশ্ন যুক্ত করুন
;
})()}
{/* 3. RIGHT SIDEBAR: Live A4 Preview & Layout */}
setRightSidebarTab('preview')} className={`px-3 py-1.5 rounded-md font-bold transition ${rightSidebarTab==='preview'?'bg-emerald-100 text-emerald-800':'text-slate-600 hover:bg-slate-100'}`}>📄 লাইভ প্রিভিউ
setRightSidebarTab('layout')} className={`px-3 py-1.5 rounded-md font-bold transition ${rightSidebarTab==='layout'?'bg-emerald-100 text-emerald-800':'text-slate-600 hover:bg-slate-100'}`}>⚙️ লেআউট সেটিংস
{rightSidebarTab === 'preview' && (
setPreviewZoom(z => Math.max(0.3, +(z-0.1).toFixed(2)))} className="px-2 py-0.5 rounded bg-white font-bold hover:bg-slate-200 text-slate-700">-
{Math.round(previewZoom*100)}%
setPreviewZoom(z => Math.min(1.5, +(z+0.1).toFixed(2)))} className="px-2 py-0.5 rounded bg-white font-bold hover:bg-slate-200 text-slate-700">+
)}
{rightSidebarTab === 'preview' ? (
) : (
)}
>
)}
>)}
{/* Offscreen perfectly scaled PDF & DOCX print stage */}
{bankModal && setBankModal(null)} activePaper={activePaper} onInsert={insertBankEntries} showToast={showToast} />}
setConfirmDlg(null)} />
{toast && (
{toast.msg}
{toast.undo && পূর্বাবস্থায় ফেরান }
)}
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);