// 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: `ABCD` }, { id: 'circle', name: 'বৃত্ত ও ব্যাসার্ধ (Circle)', svg: `OABr` }, { id: 'rect', name: 'আয়ত ABCD (Rectangle)', svg: `ABCD` }, { id: 'right_tri', name: 'সমকোণী ত্রিভুজ (Right Triangle)', svg: `ABC` }, { id: 'transversal', name: 'সমান্তরাল রেখা ও ছেদক', svg: `1234ABCD` }, { id: 'numline', name: 'সংখ্যারেখা (Number Line)', svg: `-3-2-10123` }, { id: 'circuit', name: 'বৈদ্যুতিক বর্তনী (Circuit)', svg: `বাসুইচব্যাটারি` }, { id: 'magnet', name: 'দণ্ড চুম্বক (Bar Magnet)', svg: `NS` }, { 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.stopPropagation()} className="bg-white rounded-2xl shadow-2xl w-full max-w-sm p-5">
{dlg.icon || '🗑️'}

{dlg.title}

{dlg.message}

); } function QuestionNav({ paper, activeQ, navOpen, setNavOpen, onSelect, onToggle, onAdd, onReorder, onPick }) { const [dragInfo, setDragInfo] = useState(null); return (
{sectionOrderFor(paper).map((k) => { const sec = paper.sections?.[k] || { items: [] }; const t = sec.title || SECTION_TITLES[k] || 'কাস্টম বিভাগ'; const items = sec.items || []; const open = !!navOpen[k]; return (
onToggle(k, !sec.enabled)} title="প্রশ্নপত্রে অন্তর্ভুক্ত করুন" className="accent-emerald-600 w-5 h-5 shrink-0 rounded cursor-pointer" />
{open && (
{items.length === 0 &&
এখনো কোনো প্রশ্ন নেই
} {items.map((it, i) => { const txt = plainText(it.text || it.stimulus) || 'খালি প্রশ্ন...'; const on = activeQ === it.id; return (
{ setDragInfo({ sec: k, idx: i }); e.dataTransfer.effectAllowed = 'move'; }} onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }} onDrop={(e) => { e.preventDefault(); if (dragInfo && dragInfo.sec === k) onReorder(k, dragInfo.idx, i); setDragInfo(null); }} className={`w-full flex items-start gap-1.5 text-left px-2.5 py-2.5 rounded-lg text-[13px] transition cursor-move ${on ? 'bg-emerald-600 text-white font-semibold shadow-sm' : 'hover:bg-slate-100 text-slate-700'}`} title="ড্রাগ করে সরাতে পারেন" >
); })}
{onPick && }
)}
); })}
); } // --- REUSABLE COMPONENTS --- function RichTextField({ value, onChange, placeholder = 'এখানে লিখুন...', minHeight = '38px', compact = false, lite = false }) { const editorRef = useRef(null); const [showSymbols, setShowSymbols] = useState(false); useEffect(() => { if (editorRef.current && editorRef.current.innerHTML !== (value || '')) { editorRef.current.innerHTML = value || ''; } }, [value]); const execCmd = (cmd, arg = null) => { editorRef.current?.focus(); document.execCommand(cmd, false, arg); if (editorRef.current) onChange(editorRef.current.innerHTML); }; const insertSymbol = (sym) => { editorRef.current?.focus(); document.execCommand('insertText', false, sym); if (editorRef.current) onChange(editorRef.current.innerHTML); }; const SYMBOLS = ['H₂O', 'CO₂', 'O₂', 'H₂SO₄', 'NaCl', 'CaCO₃', '×', '÷', '±', '√', 'π', '°', '²', '³', '→', '⇌', 'Δ', 'θ', '∠', '△', '≤', '≥', '≠']; return (
{showSymbols && (
{SYMBOLS.map((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 }) => (
{onDelete && }
); return (
{currentImage && ( <>
)}
{open && (
setOpen(false)}>
e.stopPropagation()} className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[88vh] flex flex-col">

চিত্র নির্বাচন করুন

আমার আপলোড ({toBengaliNumber(saved.length)})
{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 && (
)}
{statements.map((st, i) => (
{ROMAN[i]}
set(i, v)} placeholder={`বিবৃতি ${ROMAN[i]} লিখুন...`} compact lite minHeight="30px" />
))} {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) => ( ))}
{question.options.map((opt, idx) => { const isCorrect = question.correctIndex === idx; const isExp = expandedOpt === idx; return (
{/* এখানে সাধারণ ইনপুটের বদলে RichTextField ব্যবহার করা হলো */}
updateOption(idx, { text: val })} compact={true} lite={true} minHeight="34px" placeholder={`অপশন ${labels[idx]}...`} />
{isExp && (
অ্যালাইনমেন্ট:
ফন্ট সাইজ:
)}
); })}
); } 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.image &&
` }} />}
{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 (
{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 (

সেটিংস ও কন্ট্রোল প্যানেল

🔑 আপনার পাসওয়ার্ড পরিবর্তন করুন

setOldPass(e.target.value)} className="w-full border border-slate-300 rounded p-2" required />
setNewPass(e.target.value)} className="w-full border border-slate-300 rounded p-2" required minLength="6" />
{auth.role === 'superadmin' && (

🛡️ অ্যাডমিন কন্ট্রোল: নতুন ইউজার তৈরি

setNewUser({...newUser, username: e.target.value})} className="w-full border border-slate-300 rounded p-2" required />
setNewUser({...newUser, password: e.target.value})} className="w-full border border-slate-300 rounded p-2" required minLength="6" />

সিস্টেম ইউজার তালিকা ({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" />
{users.map(u => ( ))} {users.length === 0 && ( )}
ইউজারনেম রোল (Role) অ্যাকাউন্ট Question Bank permissions তৈরির তারিখ অ্যাকশন / Activity
{u.username} {u.role.toUpperCase()}
{u.is_active ? '● Active' : '● Disabled'}
{u.disable_at &&
বন্ধ হবে: {new Date(u.disable_at).toLocaleString()}
} {u.username !== 'admin' &&
setScheduleDates(s => ({ ...s, [u.id]: e.target.value }))} className="border rounded px-1 py-1 text-[10px]" title="এই তারিখের শেষে অ্যাকাউন্ট বন্ধ হবে" /> {u.disable_at && }
}
{u.role === 'superadmin' ? পূর্ণ অনুমতি :
{[['add', 'can_add_question_bank', 'প্রশ্ন যোগ'], ['edit', 'can_edit_question_bank', 'প্রশ্ন সম্পাদনা'], ['delete', 'can_delete_question_bank', 'প্রশ্ন মুছতে']].map(([permission, key, label]) => )}
}
{new Date(u.created_at).toLocaleDateString()}
{u.username !== 'admin' && }
কোনো ইউজার নেই
{userPagination.last_page > 1 &&
পৃষ্ঠা {toBengaliNumber(userPagination.current_page)} / {toBengaliNumber(userPagination.last_page)}
}
)} {activityUser &&
setActivityUser(null)}>
e.stopPropagation()}>

{activityUser.username} — Activity history

ইউজার যোগ ও status পরিবর্তনের রেকর্ড
{(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)}
কর্তা: {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 &&
পৃষ্ঠা {toBengaliNumber(activity.current_page)} / {toBengaliNumber(activity.last_page)}
}
}
); } // --- 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]) => ( ))}
{showMarks && ( )}
প্রশ্ন
set({ text: v })} placeholder="প্রশ্ন লিখুন..." minHeight="50px" />
{q.type === 'statement' && (
set({ statements: sts })} onLayoutChange={m => set({ statementLayoutMode: m })} />
সমাপ্তি বাক্য: set({ subPrompt: e.target.value })} className="flex-1 border border-slate-300 rounded px-2 py-1 text-sm outline-none focus:border-emerald-500" placeholder="নিচের কোনটি সঠিক?" />
)} 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]} লিখুন...`} />
); })}
); } // ---------- 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 (
📄 অভিন্ন উদ্দীপক (Shared Stimulus) — গ্রুপের সব MCQ-এর জন্য একবারই সংরক্ষিত হবে
onChange({ ...group, stimulusInstruction: e.target.value })} className="w-full border border-slate-300 rounded px-3 py-1.5 text-sm outline-none bg-white focus:border-amber-500" placeholder="নির্দেশনা (ঐচ্ছিক) — ফাঁকা রাখলে পরীক্ষায় স্বয়ংক্রিয় লেখা বসবে" /> onChange({ ...group, sharedStimulus: v })} placeholder="উদ্দীপক / প্যাসেজ এখানে লিখুন..." minHeight="70px" /> onChange({ ...group, stimulusImage: img })} />
{qs.map((q, i) => { const isClosed = closed[q.id]; return (
MCQ {toBengaliNumber(i + 1)}{plainText(q.text) ? ` — ${plainText(q.text).slice(0, 30)}` : ''}
{!isClosed && setQ(i, nq)} />}
); })}
); } 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) => (
{s.label}
setSub(i, { text: v })} placeholder={`${s.label} নং প্রশ্ন...`} compact lite minHeight="38px" />
setSub(i, { marks: e.target.value === '' ? '' : Number(e.target.value) })} className="w-14 mt-1 border border-slate-200 rounded-md px-1 py-1.5 text-sm text-center outline-none focus:border-emerald-500" title="মান" />
))}
); } 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 (
{onAdd && }
{open && (
setName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); submit(); } }} placeholder="নতুন নাম লিখুন" className="flex-1 min-w-[120px] border rounded px-2 py-1 text-sm outline-none focus:border-emerald-500" /> {withCode && setCode(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); submit(); } }} placeholder="বিষয় কোড (ঐচ্ছিক)" className="w-36 border rounded px-2 py-1 text-sm outline-none focus:border-emerald-500" />}
)}
); } // ---------- 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})} 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 => )}
)}
); } // ---------- 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}
  • )}
)}
{BANK_TYPES.map(t => ( ))}
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 })))} />
{ setMarksTouched(true); setForm(f => ({ ...f, marks: e.target.value === '' ? '' : Number(e.target.value) })); }} className="w-full border rounded p-2 text-sm outline-none focus:ring-1 focus:ring-emerald-500 disabled:bg-slate-100" title={form.q_type === 'MCQ' && mode === 'group' ? 'গ্রুপের সব MCQ-এর মানের যোগফল' : ''} />
setForm(f => ({ ...f, tags }))} />
{form.q_type === 'MCQ' && ( <>
গ্রুপ শুধু MCQ-এর জন্য — সৃজনশীল ও সংক্ষিপ্ত প্রশ্নে গ্রুপ নেই।
{mode === 'group' ? : } )} {form.q_type === 'Creative' && } {form.q_type === 'Short' && } {form.q_type === 'Other' && }
{!isEdit && } {(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 (
{expanded && }
); } 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}` : ''}} {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 => )}
}
তৈরি: {fmtDate(entry.created_at)} · সংশোধন: {fmtDate(entry.updated_at)}
{!picker && permissions.edit && <> } {!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 && } {canAdd && }
); } 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) => ; 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)}
; } // ---------- 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) => )} {importDraft.preview &&
প্রিভিউ
{importDraft.preview}
}

শ্রেণি, বিষয়, অধ্যায় বা ট্যাগ না থাকলে ইমপোর্টের সময় তৈরি হবে।

}
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" /> {!picker && <> {canAdd && <> }}
{showFilters && (
patch({ marks_min: e.target.value })} /> patch({ marks_max: e.target.value })} />
patch({ created_from: e.target.value })} /> patch({ created_to: e.target.value })} />
patch({ updated_from: e.target.value })} /> patch({ updated_to: e.target.value })} />
{meta.tags.length === 0 && এখনো কোনো ট্যাগ নেই} {meta.tags.map(t => { const on = (f.tag_ids || []).includes(t.id); return ; })}
)} {chips.length > 0 && (
সক্রিয় ফিল্টার: {chips.map(c => {c.label})} {(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}
  • )}
}
)}
মোট {toBengaliNumber(data.total)}টি প্রশ্ন{chips.length > 0 ? ' (ফিল্টার অনুযায়ী)' : ''} {loading && লোড হচ্ছে...}
সাজান:
{selIds.length > 0 && (
{toBengaliNumber(selIds.length)}টি নির্বাচিত {!picker && canEdit && <> } {!picker && canAdd && !canEdit && } {!picker && canDelete && }
)} {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 && (
{pageNums[0] > 1 && <>…} {pageNums.map(n => )} {pageNums[pageNums.length - 1] < data.pages && <>…}
)} {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 => (
{editId === it.id ? (
setEName(e.target.value)} onKeyDown={e => e.key === 'Enter' && saveEdit(it)} className="flex-1 min-w-[80px] border rounded px-1.5 py-0.5 text-sm outline-none" /> {withCode && setECode(e.target.value)} placeholder="কোড" className="w-20 border rounded px-1.5 py-0.5 text-sm outline-none" />}
) : (
{canEdit && } {canDelete && }
)}
))}
)}
); } 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" />
}
{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" /> ) : ( <> #{t.name} {toBengaliNumber(t.cnt)}টি প্রশ্ন {permissions.can_edit_question_bank && } {permissions.can_delete_question_bank && } )}
))}
); } // ---------- 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]) => ( ))}
যুক্ত হবে:
{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)}টি যুক্ত হয়েছে` : ''}` : 'শ্রেণি, বিষয় ও অধ্যায় বাছাই করে সেভ করুন — পেজ ছাড়তে হবে না'}
{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 (

সিস্টেম লগইন

setLoginForm({...loginForm, username: e.target.value})} required />
setLoginForm({...loginForm, password: e.target.value})} required />
); } if (papersLoadError) return

ড্যাশবোর্ড লোড হয়নি

{papersLoadError}

; 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()}
{saveState === 'saved' ? '✓ সংরক্ষিত' : saveState === 'error' ? '⚠ সেভ ব্যর্থ' : '● সংরক্ষণ হচ্ছে...'} {appView === 'editor' && } {appView === 'editor' && } {appView === 'editor' && } {appView === 'editor' && } {appView === 'editor' && }
{appView === 'bank' && ( setBankPrefill(null)} hint={activePaper ? activePaper.header : null} showToast={showToast} /> )} {appView === 'editor' && (<>
{[['nav', 'প্রশ্ন তালিকা'], ['edit', 'এডিটর'], ['preview', 'প্রিভিউ']].map(([id, l]) => )}
{/* 1. LEFT SIDEBAR */} {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 => ( ))}
{sectionOrderFor(activePaper).map((key, i, order) => {activePaper.sections[key]?.title || SECTION_TITLES[key] || 'কাস্টম বিভাগ'}{i > 0 && }{i < order.length - 1 && })}
{activeTab === 'header' && (

প্রতিষ্ঠানের তথ্য ও পরীক্ষার শিরোনাম

updateActivePaper({title: e.target.value})} className="w-full border rounded p-2 text-sm font-bold text-emerald-800 focus:ring-1 focus:ring-emerald-500 outline-none" />
updateHeader({institutionName: e.target.value})} className="flex-1 border rounded p-2 text-sm font-bold outline-none focus:ring-1 focus:ring-emerald-500" />
updateHeader({examName: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1" />
updateHeader({className: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1" />
updateHeader({subject: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1" />
updateHeader({duration: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1" />
updateHeader({totalMarks: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1" />
updateHeader({subjectCode: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1" />
)} {activeTab === 'mcq' && (

বহুনির্বাচনী প্রশ্ন (MCQ) সেটিংস

কলাম:
updateSection('mcq', {title: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1 focus:ring-emerald-500" />
updateSection('mcq', {marksText: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1 focus:ring-emerald-500" />
updateSection('mcq', {instruction: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1 focus:ring-emerald-500" />
{(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' : ''}`}>
{toBengaliNumber(idx+1)}. {getQuestionPreview(item)}
{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}
updateSection('mcq', { items: activePaper.sections.mcq.items.map((q,i) => i===idx ? {...item, stimulusQCount: Number(e.target.value)||2} : q) })} className="w-16 border border-slate-300 rounded px-2 py-1 text-sm outline-none text-center focus:border-amber-500" />
updateSection('mcq', { items: activePaper.sections.mcq.items.map((q,i) => i===idx ? {...item, stimulusInstruction: e.target.value} : q) })} className="flex-1 border border-slate-300 rounded px-3 py-1.5 text-sm outline-none text-slate-600 bg-white focus:border-amber-500" placeholder="নির্দেশনা লিখুন..." />
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, text: val} : q) })} /> {item.type === 'statement' && (
updateSection('mcq', { items: activePaper.sections.mcq.items.map((q,i) => i===idx ? {...item, statements: sts} : q) })} onLayoutChange={mode => updateSection('mcq', { items: activePaper.sections.mcq.items.map((q,i) => i===idx ? {...item, statementLayoutMode: mode} : q) })} />
সমাপ্তি বাক্য: updateSection('mcq', { items: activePaper.sections.mcq.items.map((q,i) => i===idx ? {...item, subPrompt: e.target.value} : q) })} className="flex-1 border border-slate-300 rounded px-2 py-1 text-sm outline-none focus:border-emerald-500 font-serifBn" placeholder="যেমন: নিচের কোনটি সঠিক?" />
)} 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) })} /> )}
); })}
)} {activeTab === 'creative' && (

সৃজনশীল প্রশ্ন (Creative Questions) সেটিংস

কলাম:
updateSection('creative', {title: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1 focus:ring-emerald-500" />
updateSection('creative', {marksText: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1 focus:ring-emerald-500" />
updateSection('creative', {instruction: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1 focus:ring-emerald-500" />
{(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'}`}>
সৃজনশীল {toBengaliNumber(idx+1)}. {getQuestionPreview(item)}
{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) => (
{sq.label}
{ const newSq = [...item.subQuestions]; newSq[sIdx].text = val; updateSection('creative', { items: activePaper.sections.creative.items.map((q,i) => i===idx ? {...item, subQuestions: newSq} : q) }); }} placeholder={`${sq.label} নং প্রশ্নের উত্তর...`} compact lite minHeight="38px" />
{ const newSq = [...item.subQuestions]; newSq[sIdx].marks=e.target.value; updateSection('creative', { items: activePaper.sections.creative.items.map((q,i) => i===idx ? {...item, subQuestions: newSq} : q) }); }} className="w-14 mt-1 border border-slate-200 rounded-md px-1 py-1.5 text-sm text-center outline-none focus:border-emerald-500 shadow-sm" title="মান" />
))}
)}
); })}
)} {activeTab === 'short' && (

সংক্ষিপ্ত প্রশ্ন (Short Questions) সেটিংস

কলাম:
updateSection('short', {title: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1 focus:ring-emerald-500" />
updateSection('short', {marksText: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1 focus:ring-emerald-500" />
updateSection('short', {instruction: e.target.value})} className="w-full border rounded p-2 text-sm outline-none focus:ring-1 focus:ring-emerald-500" />
{(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 />
))}
)} {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
কলাম:{[1,2].map(n => )}
{items.map((item, idx) =>
প্রশ্ন {toBengaliNumber(idx+1)}
setItems(items.map((q,i) => i === idx ? { ...q, text: val } : q))} compact />
)}
; })()}
{/* 3. RIGHT SIDEBAR: Live A4 Preview & Layout */} )}
)} {/* 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();