255 lines
13 KiB
JavaScript
255 lines
13 KiB
JavaScript
/* 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);
|
||
})();
|