/* Progressive enhancement: native forms and private-ticket links work without this script. */ (() => { 'use strict'; const number = new Intl.NumberFormat('fa-IR'); const displayCount = count => `${number.format(count)} نفر`; function initSections(root) { const tabs = Array.from(root.querySelectorAll('[data-dispatch-tab]')); const panels = Array.from(root.querySelectorAll('[data-dispatch-panel]')); const hasErrors = Boolean(root.querySelector('.dispatch-error-summary')); const filters = new URLSearchParams(location.search); const hasFilters = ['q', 'status', 'unit', 'group', 'created_from', 'created_to', 'page'].some(key => filters.get(key)); const selectedSection = () => { if (hasErrors) return 'compose'; if (location.hash === '#dispatch-history-panel') return 'history'; if (location.hash === '#dispatch-compose-panel') return 'compose'; return hasFilters ? 'history' : 'compose'; }; const show = section => { tabs.forEach(tab => { const active = tab.dataset.dispatchTab === section; tab.setAttribute('aria-selected', String(active)); tab.setAttribute('aria-current', String(active)); tab.tabIndex = active ? 0 : -1; }); panels.forEach(panel => { panel.hidden = panel.dataset.dispatchPanel !== section; }); }; root.querySelector('.dispatch-view-switch').setAttribute('role', 'tablist'); tabs.forEach((tab, index) => { tab.setAttribute('role', 'tab'); tab.setAttribute('aria-controls', `dispatch-${tab.dataset.dispatchTab}-panel`); tab.addEventListener('click', event => { event.preventDefault(); history.pushState(null, '', tab.getAttribute('href')); show(tab.dataset.dispatchTab); }); tab.addEventListener('keydown', event => { let next; if (event.key === 'ArrowLeft') next = (index + 1) % tabs.length; if (event.key === 'ArrowRight') next = (index + tabs.length - 1) % tabs.length; if (event.key === 'Home') next = 0; if (event.key === 'End') next = tabs.length - 1; if (next === undefined) return; event.preventDefault(); tabs[next].click(); tabs[next].focus(); }); }); panels.forEach(panel => panel.setAttribute('role', 'tabpanel')); window.addEventListener('popstate', () => show(selectedSection())); show(selectedSection()); } function fillOptions(select, items, labelKey, placeholder, autoSelect = false) { const previous = select.value; select.replaceChildren(new Option(placeholder, '')); items.forEach(item => select.add(new Option(item[labelKey], String(item.id)))); if (items.some(item => String(item.id) === previous)) select.value = previous; else if (autoSelect && items.length === 1) select.value = String(items[0].id); } function initRecipients(root, form, data) { const operation = form.elements.operation_type; const unit = form.elements.unit; const group = form.elements.contact_group; const category = form.elements.category; const groupHint = root.querySelector('[data-dispatch-group-hint]'); const text = (selector, value) => { root.querySelector(selector).textContent = value; }; const updateSummary = () => { const selected = data.groups.find(item => String(item.id) === group.value); updateResponseOptions(root, form, selected); text('[data-dispatch-unit-summary]', unit.value ? unit.selectedOptions[0].textContent : 'انتخاب نشده'); text('[data-dispatch-group-summary]', selected ? selected.name : 'انتخاب نشده'); text('[data-dispatch-count-summary]', selected ? displayCount(selected.recipient_count) : 'پس از انتخاب گروه'); group.setCustomValidity(selected && !selected.recipient_count ? 'این گروه مخاطب فعال دارای شماره همراه ندارد؛ گروه دیگری انتخاب کنید.' : ''); groupHint.textContent = selected ? `${displayCount(selected.recipient_count)} فعال دارای شماره همراه؛ تعداد نهایی هنگام ثبت بررسی می‌شود.` : 'فقط گروه‌های واحد انتخاب‌شده نمایش داده می‌شوند.'; }; const updateGroupOptions = () => { const hasUnit = Boolean(unit.value); const groups = data.groups.filter(item => String(item.unit_id) === unit.value); const categories = data.categories.filter(item => String(item.unit_id) === unit.value); const applicableCategories = categories.length ? categories : data.categories.filter(item => item.unit_id === null); fillOptions(group, hasUnit ? groups : [], 'name', hasUnit ? 'گروه مخاطبان را انتخاب کنید' : 'ابتدا واحد را انتخاب کنید'); fillOptions(category, hasUnit ? applicableCategories : [], 'title', hasUnit ? 'دسته‌بندی را انتخاب کنید' : 'ابتدا واحد را انتخاب کنید'); group.disabled = !hasUnit; category.disabled = !hasUnit; updateSummary(); }; const updateUnitOptions = () => { const units = data.units.filter(item => item.operations.includes(operation.value)); fillOptions(unit, units, 'name', 'واحد ارسال‌کننده را انتخاب کنید', !root.querySelector('.dispatch-error-summary')); updateGroupOptions(); }; operation.addEventListener('change', updateUnitOptions); unit.addEventListener('change', updateGroupOptions); group.addEventListener('change', updateSummary); updateUnitOptions(); return () => data.groups.find(item => String(item.id) === group.value); } function updateResponseOptions(root, form, group) { const sms = form.elements.notify_opening_sms; const individual = form.elements.response_mode.value === 'INDIVIDUAL'; const oversized = Boolean(group && group.recipient_count > 15); if (sms.dataset.openingPreference === undefined) sms.dataset.openingPreference = String(sms.checked); sms.disabled = individual || oversized; sms.checked = !sms.disabled && sms.dataset.openingPreference === 'true'; root.querySelector('[data-opening-sms-hint]').textContent = individual ? 'پیامک بازگشایی فقط در حالت یکجا مجاز است.' : oversized ? 'بیش از ۱۵ گیرندهٔ واجد شرایط؛ اطلاع‌رسانی بازگشایی با پیامک مجاز نیست.' : 'پس از بازگشایی فقط پاسخ‌دهندگان پیامک دریافت می‌کنند؛ حداکثر ۱۵ گیرنده مجاز است.'; } function initReview(root, form, selectedGroup) { const dialog = root.querySelector('[data-dispatch-review]'); if (typeof dialog.showModal !== 'function') return; const send = form.querySelector('[data-dispatch-send]'); const confirm = dialog.querySelector('[data-dispatch-confirm]'); let confirmed = false; send.textContent = 'بررسی و ادامه'; const setText = (selector, value) => { dialog.querySelector(selector).textContent = value; }; const review = () => { const group = selectedGroup(); if (!form.reportValidity() || !group) return; // The shared uploader empties picker inputs only after image preparation completes. if (Array.from(form.querySelectorAll('[data-gallery-input], [data-document-input]')).some(input => input.files.length)) { root.querySelector('[data-compression-status]').textContent = 'آماده‌سازی پیوست‌ها ادامه دارد؛ پس از پایان دوباره بررسی و ادامه را بزنید.'; return; } setText('[data-review-unit]', form.elements.unit.selectedOptions[0].textContent); setText('[data-review-group]', group.name); setText('[data-review-count]', displayCount(group.recipient_count)); setText('[data-review-response-policy]', `${form.elements.response_mode.value === 'SEALED' ? 'بازگشایی یکجا' : 'مشاهده جداگانه'}؛ پیامک بازگشایی: ${form.elements.notify_opening_sms.checked && !form.elements.notify_opening_sms.disabled ? 'فعال' : 'غیرفعال'}`); dialog.querySelector('[data-review-inquiry-recommendation]').hidden = form.elements.response_mode.value === 'SEALED' && form.elements.notify_opening_sms.checked && !form.elements.notify_opening_sms.disabled; dialog.querySelector('[data-review-inquiry-limit]').hidden = group.recipient_count <= 15; const files = Array.from(form.elements.files.files); setText('[data-review-files]', files.length ? files.map(file => file.name).join('، ') : 'بدون پیوست'); setText('[data-review-subject]', form.elements.subject.value); setText('[data-review-body]', form.elements.body.value); dialog.showModal(); dialog.querySelector('[data-dispatch-review-close]').focus(); }; // Capture before shared uploader/UI listeners: reviewing must not disable submission controls. form.addEventListener('submit', event => { if (confirmed) return; event.preventDefault(); event.stopImmediatePropagation(); review(); }, true); dialog.querySelectorAll('[data-dispatch-review-close]').forEach(button => button.addEventListener('click', () => dialog.close())); dialog.addEventListener('close', () => { if (!confirmed) send.focus(); }); confirm.addEventListener('click', () => { if (confirmed || !form.reportValidity()) return; confirmed = true; confirm.disabled = true; confirm.textContent = 'در حال ثبت…'; dialog.close(); form.requestSubmit(send); }); } function initErrors(root, form) { form.querySelectorAll('.errorlist').forEach((list, index) => { const control = list.parentElement.querySelector('input, select, textarea'); if (!control) return; list.id = `dispatch-error-${index}`; control.setAttribute('aria-invalid', 'true'); control.setAttribute('aria-describedby', list.id); }); const summary = root.querySelector('.dispatch-error-summary'); if (!summary) return; summary.querySelectorAll('a').forEach(link => link.addEventListener('click', event => { const control = document.getElementById(link.hash.slice(1)); if (!control) return; event.preventDefault(); const disclosure = control.closest('details'); if (disclosure) disclosure.open = true; control.focus(); })); summary.focus(); } function initComposer(root) { const form = root.querySelector('#dispatch-compose-form'); const data = JSON.parse(root.querySelector('#dispatch-options').textContent); initSections(root); const selectedGroup = initRecipients(root, form, data); form.elements.notify_opening_sms.addEventListener('change', event => { event.target.dataset.openingPreference = String(event.target.checked); }); form.querySelectorAll('[name="response_mode"]').forEach(input => input.addEventListener('change', () => updateResponseOptions(root, form, selectedGroup()))); initReview(root, form, selectedGroup); initErrors(root, form); // Visible, keyboard-operable file labels retain the shared uploader's original inputs. root.querySelectorAll('.media-picker-button').forEach(label => { label.tabIndex = 0; label.setAttribute('role', 'button'); label.addEventListener('keydown', event => { if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); document.getElementById(label.htmlFor).click(); }); }); } function normalizeSearch(value) { return value.toLowerCase().replace(/[يى]/g, 'ی').replace(/ك/g, 'ک') .replace(/[۰-۹]/g, digit => String(digit.charCodeAt(0) - 1776)) .replace(/[٠-٩]/g, digit => String(digit.charCodeAt(0) - 1632)) .replace(/[\s\u200c-]+/g, ''); } function initReport(root) { const rows = Array.from(root.querySelectorAll('[data-recipient-row]')); if (!rows.length) return; const tools = root.querySelector('[data-recipient-tools]'); const search = tools.querySelector('[data-recipient-search]'); const filter = tools.querySelector('[data-recipient-filter]'); const status = tools.querySelector('[data-recipient-result-count]'); const empty = root.querySelector('[data-recipient-empty]'); const matches = { all: () => true, answered: row => row.dataset.answered === 'true', unanswered: row => row.dataset.answered === 'false', opened: row => row.dataset.opened === 'true', unopened: row => row.dataset.opened === 'false', retryable: row => row.dataset.retryable === 'true', active: row => row.dataset.active === 'true', }; const update = () => { const query = normalizeSearch(search.value); let visible = 0; rows.forEach(row => { row.hidden = !normalizeSearch(row.dataset.search).includes(query) || !matches[filter.value](row); if (!row.hidden) visible += 1; }); status.textContent = `${number.format(visible)} از ${number.format(rows.length)} مخاطب`; empty.hidden = visible > 0; }; search.addEventListener('input', update); filter.addEventListener('change', update); root.querySelectorAll('[data-recipient-reset]').forEach(button => button.addEventListener('click', () => { search.value = ''; filter.value = 'all'; update(); search.focus(); })); tools.hidden = false; update(); } document.querySelectorAll('[data-dispatch-workspace]').forEach(initComposer); document.querySelectorAll('[data-dispatch-report]').forEach(initReport); })();