estelam ticket, varizha , others
This commit is contained in:
1 parent
35a1ec1794
commit
6b546aad76
71 files changed
+2865
-191
No files matched your search
@@ -0,0 +1,254 @@
|
||||
/* 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);
|
||||
})();
|
||||
Reference in new issue
Block a user