add payment_link and banks
This commit is contained in:
1 parent
2ae79b1ca4
commit
de3135b588
100 files changed
+6167
-47
No files matched your search
@@ -0,0 +1,25 @@
|
||||
/* Selection changes presentation only; permissions and updates remain server-side. */
|
||||
(() => {
|
||||
'use strict';
|
||||
const form = document.querySelector('[data-bank-selection]');
|
||||
if (!form) return;
|
||||
const boxes = Array.from(form.querySelectorAll('input[name="accounts"]'));
|
||||
const all = form.querySelector('[data-select-all]');
|
||||
const count = form.querySelector('[data-selection-count]');
|
||||
const submit = form.querySelector('[data-bulk-submit]');
|
||||
if (!all || !count || !submit) return;
|
||||
const update = () => {
|
||||
const selected = boxes.filter(box => box.checked).length;
|
||||
all.checked = selected > 0 && selected === boxes.length;
|
||||
all.indeterminate = selected > 0 && selected < boxes.length;
|
||||
count.textContent = selected ? `${selected.toLocaleString('fa-IR')} حساب انتخاب شده` : 'حسابهای موردنظر را انتخاب کنید.';
|
||||
submit.disabled = selected === 0;
|
||||
};
|
||||
all.addEventListener('change', () => {
|
||||
boxes.forEach(box => { box.checked = all.checked; });
|
||||
update();
|
||||
});
|
||||
boxes.forEach(box => box.addEventListener('change', update));
|
||||
window.addEventListener('pageshow', update);
|
||||
update();
|
||||
})();
|
||||
@@ -0,0 +1,94 @@
|
||||
/* Local, server-filtered search. Explicit choice only; no customer data in storage. */
|
||||
(() => {
|
||||
'use strict';
|
||||
const root = document.querySelector('[data-payment-customer-picker]');
|
||||
if (!root) return;
|
||||
const search = root.querySelector('input[type=search]');
|
||||
const value = root.querySelector('#id_customer');
|
||||
const results = root.querySelector('[data-customer-results]');
|
||||
const selected = root.querySelector('[data-selected-customer]');
|
||||
const status = root.querySelector('[role=status]');
|
||||
let timer, controller, version = 0;
|
||||
const node = (tag, text, className) => {
|
||||
const element = document.createElement(tag);
|
||||
if (text !== undefined) element.textContent = text;
|
||||
if (className) element.className = className;
|
||||
return element;
|
||||
};
|
||||
const history = customer => {
|
||||
const details = node('details', undefined, 'pay-customer-history');
|
||||
details.append(node('summary', `سوابق قابل مشاهده · ${customer.ticket_count.toLocaleString('fa-IR')} تیکت`));
|
||||
if (!customer.history.length) details.append(node('p', 'سابقه تیکت قابل مشاهدهای وجود ندارد.'));
|
||||
customer.history.forEach(ticket => {
|
||||
const link = node('a', `${ticket.number} · ${ticket.subject}`);
|
||||
link.href = ticket.url; link.target = '_blank'; link.rel = 'noopener';
|
||||
const row = node('div'); row.append(link, node('small', ticket.status)); details.append(row);
|
||||
});
|
||||
return details;
|
||||
};
|
||||
const identity = customer => {
|
||||
const container = node('div', undefined, 'pay-customer-identity');
|
||||
container.append(node('strong', customer.name), node('bdi', customer.mobile));
|
||||
if (customer.organization) container.append(node('span', customer.organization));
|
||||
container.append(node('small', customer.location || 'موقعیت ثبت نشده'));
|
||||
return container;
|
||||
};
|
||||
const choose = (customer, focus = true) => {
|
||||
clearTimeout(timer); version++; controller?.abort();
|
||||
value.value = customer.value; value.dispatchEvent(new Event('change', {bubbles:true}));
|
||||
search.value = ''; results.hidden = true; results.replaceChildren();
|
||||
selected.replaceChildren(node('span', 'مشتری انتخابشده', 'pay-selected-label'), identity(customer), history(customer));
|
||||
const change = node('button', 'تغییر مشتری', 'pay-button secondary'); change.type = 'button';
|
||||
change.addEventListener('click', () => {
|
||||
value.value = ''; value.dispatchEvent(new Event('change', {bubbles:true})); selected.hidden = true; search.focus();
|
||||
status.textContent = 'مشتری جدید را جستوجو و انتخاب کنید.';
|
||||
});
|
||||
selected.append(change); selected.hidden = false;
|
||||
status.textContent = 'قبل از صدور، نام و شماره تماس مشتری انتخابشده را بررسی کنید.';
|
||||
if (focus) change.focus({preventScroll:true});
|
||||
};
|
||||
const lookup = async () => {
|
||||
const query = search.value.trim(), current = ++version;
|
||||
controller?.abort(); controller = new AbortController();
|
||||
if (query.length < 2) { results.hidden = true; status.textContent = 'حداقل دو حرف یا رقم وارد کنید.'; return; }
|
||||
status.textContent = 'در حال جستوجو…';
|
||||
try {
|
||||
const response = await fetch(`${root.dataset.searchUrl}?q=${encodeURIComponent(query)}`, {
|
||||
credentials: 'same-origin', cache: 'no-store', signal: controller.signal
|
||||
});
|
||||
if (!response.ok) throw new Error('lookup');
|
||||
const data = await response.json();
|
||||
if (current !== version) return;
|
||||
results.replaceChildren();
|
||||
data.results.forEach(customer => {
|
||||
const card = node('article', undefined, 'pay-customer-result');
|
||||
const button = node('button', 'انتخاب این مشتری', 'pay-button secondary'); button.type = 'button';
|
||||
button.addEventListener('click', () => choose(customer));
|
||||
card.append(identity(customer), history(customer), button); results.append(card);
|
||||
});
|
||||
results.hidden = !data.results.length;
|
||||
status.textContent = data.results.length ? `${data.results.length.toLocaleString('fa-IR')} نتیجه${data.has_more ? '؛ برای نتایج دقیقتر نام یا شماره کاملتری وارد کنید.' : '؛ مشتری موردنظر را انتخاب کنید.'}` : 'مشتری فعالی پیدا نشد؛ نام یا شماره تماس را بررسی کنید.';
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError' || current !== version) return;
|
||||
results.hidden = true;
|
||||
status.textContent = 'جستوجو انجام نشد؛ اتصال را بررسی کنید و دوباره جستوجو کنید.';
|
||||
}
|
||||
};
|
||||
search.addEventListener('input', () => {
|
||||
clearTimeout(timer); version++; controller?.abort();
|
||||
value.value = ''; value.dispatchEvent(new Event('change', {bubbles:true})); selected.hidden = true; results.hidden = true;
|
||||
if (search.value.trim().length < 2) { status.textContent = 'حداقل دو حرف یا رقم وارد کنید.'; return; }
|
||||
timer = setTimeout(lookup, 350);
|
||||
});
|
||||
search.addEventListener('keydown', event => {
|
||||
if (event.key === 'Enter') { event.preventDefault(); clearTimeout(timer); lookup(); }
|
||||
});
|
||||
root.closest('form').addEventListener('submit', event => {
|
||||
if (!value.value) {
|
||||
event.preventDefault(); status.textContent = 'مشتری را از نتایج جستوجو انتخاب کنید.';
|
||||
search.focus(); search.scrollIntoView({block:'center'});
|
||||
}
|
||||
});
|
||||
const initial = JSON.parse(document.querySelector('#payment-selected-customer').textContent);
|
||||
if (initial && String(initial.value) === value.value) choose(initial, false);
|
||||
})();
|
||||
@@ -0,0 +1,44 @@
|
||||
/* Customer-only presentation. Financial and image validation remains in receipt-form.js.
|
||||
Object URLs preview local files without uploading or persisting them. */
|
||||
(() => {
|
||||
const form = document.querySelector('.customer-receipt-form');
|
||||
if (!form) return;
|
||||
const input = form.elements.namedItem('receipt_image');
|
||||
const preview = document.querySelector('#receipt-file-preview');
|
||||
const selection = document.querySelector('#receipt-file-selection');
|
||||
const status = document.querySelector('#receipt-file-status');
|
||||
let previewUrl = null;
|
||||
function releasePreview() {
|
||||
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
||||
previewUrl = null;
|
||||
preview.removeAttribute('src');
|
||||
preview.hidden = true;
|
||||
}
|
||||
function updateFile() {
|
||||
releasePreview();
|
||||
const file = input.files[0];
|
||||
selection.hidden = !file;
|
||||
status.textContent = file ? file.name + ' · ' + new Intl.NumberFormat('fa-IR', {maximumFractionDigits: 1}).format(file.size / 1024) + ' کیلوبایت' : '';
|
||||
if (file && /^image\/(jpeg|png|gif|webp)$/.test(file.type)) {
|
||||
previewUrl = URL.createObjectURL(file);
|
||||
preview.src = previewUrl;
|
||||
preview.hidden = false;
|
||||
}
|
||||
}
|
||||
input.addEventListener('change', updateFile);
|
||||
document.querySelector('#receipt-clear-file').addEventListener('click', () => {
|
||||
input.value = '';
|
||||
input.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
input.focus();
|
||||
});
|
||||
window.addEventListener('pagehide', releasePreview);
|
||||
const origin = form.elements.namedItem('origin_bank');
|
||||
const other = form.elements.namedItem('origin_other');
|
||||
function updateOrigin() {
|
||||
const visible = origin.selectedOptions[0]?.textContent.trim() === 'سایر';
|
||||
other.closest('[data-field]').hidden = !visible;
|
||||
other.disabled = !visible;
|
||||
}
|
||||
origin.addEventListener('change', updateOrigin);
|
||||
updateOrigin();
|
||||
})();
|
||||
@@ -0,0 +1,65 @@
|
||||
/* Server calculates the identifier from the selected customer's stored mobile.
|
||||
Manual values remain editable. Changing customer clears an unchanged auto value. */
|
||||
(() => {
|
||||
const button = document.querySelector('#generate-deposit-identifier');
|
||||
if (!button) return;
|
||||
const form = button.closest('form');
|
||||
const customer = form.elements.namedItem('customer');
|
||||
const input = form.elements.namedItem('deposit_id');
|
||||
const status = document.querySelector('#identifier-generation-status');
|
||||
const replacement = document.querySelector('#identifier-replacement');
|
||||
const candidate = document.querySelector('#identifier-candidate');
|
||||
const originalLabel = button.innerHTML;
|
||||
let activeCustomer = customer.value, generated = '', pending = '', controller = null;
|
||||
function customerChanged() {
|
||||
if (activeCustomer !== customer.value) {
|
||||
controller?.abort(); controller = null;
|
||||
if (generated && input.value === generated) input.value = '';
|
||||
generated = ''; pending = ''; replacement.hidden = true;
|
||||
activeCustomer = customer.value;
|
||||
}
|
||||
button.disabled = !customer.value;
|
||||
button.innerHTML = originalLabel;
|
||||
status.textContent = customer.value ? 'شناسه بر اساس شماره موبایل مشتری انتخابشده تولید میشود.' : 'ابتدا مشتری را انتخاب کنید؛ شناسه بر اساس شماره موبایل او تولید میشود.';
|
||||
}
|
||||
function applyIdentifier(value) {
|
||||
input.value = value; generated = value; pending = '';
|
||||
replacement.hidden = true;
|
||||
input.dispatchEvent(new Event('input', {bubbles:true}));
|
||||
input.dispatchEvent(new Event('change', {bubbles:true}));
|
||||
input.focus();
|
||||
status.textContent = 'شناسه تولید و در فیلد وارد شد؛ قبل از صدور اعلامیه آن را بررسی کنید.';
|
||||
}
|
||||
customer.addEventListener('change', customerChanged);
|
||||
button.addEventListener('click', async () => {
|
||||
if (!customer.value) return;
|
||||
const selectedCustomer = customer.value;
|
||||
const request = new AbortController(); controller = request;
|
||||
button.disabled = true; button.textContent = 'در حال تولید…';
|
||||
replacement.hidden = true;
|
||||
status.textContent = 'در حال دریافت شناسه از سیستم…';
|
||||
let failureMessage = 'شناسه دریافت نشد؛ اتصال را بررسی کنید و دوباره تلاش کنید.';
|
||||
try {
|
||||
const response = await fetch(button.dataset.url, {
|
||||
method: 'POST', credentials: 'same-origin', cache: 'no-store', signal: request.signal,
|
||||
headers: {'X-CSRFToken': form.elements.namedItem('csrfmiddlewaretoken').value},
|
||||
body: new URLSearchParams({customer: selectedCustomer})
|
||||
});
|
||||
const data = await response.json();
|
||||
if (request !== controller || customer.value !== selectedCustomer) return;
|
||||
if (!response.ok) {failureMessage = data.error || 'شناسه تولید نشد؛ دوباره تلاش کنید.'; throw new Error('generation');}
|
||||
if (data.customer !== selectedCustomer || !/^[0-9]{13}$/.test(data.identifier)) {failureMessage = 'پاسخ سیستم معتبر نیست؛ دوباره تلاش کنید.'; throw new Error('generation');}
|
||||
if (input.value.trim() && input.value !== data.identifier) {
|
||||
pending = data.identifier; candidate.textContent = pending; replacement.hidden = false;
|
||||
status.textContent = 'شناسه آماده است؛ برای جایگزینی مقدار فعلی، دکمه جایگزینی را بزنید.';
|
||||
} else applyIdentifier(data.identifier);
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError' && request === controller) status.textContent = failureMessage;
|
||||
} finally {
|
||||
if (request === controller) {controller = null; button.disabled = !customer.value; button.innerHTML = originalLabel;}
|
||||
}
|
||||
});
|
||||
document.querySelector('#apply-deposit-identifier').addEventListener('click', () => {if (pending) applyIdentifier(pending);});
|
||||
document.querySelector('#cancel-deposit-identifier').addEventListener('click', () => {pending = ''; replacement.hidden = true; status.textContent = 'شناسه فعلی حفظ شد.'; button.focus();});
|
||||
customerChanged();
|
||||
})();
|
||||
@@ -0,0 +1,20 @@
|
||||
/* All assets and UI behavior are local. No CDN or financial data in persistent browser storage. */
|
||||
(() => {
|
||||
'use strict';
|
||||
let toast;
|
||||
function status(text){if(!toast){toast=document.createElement('div');toast.className='pay-copy-status';toast.setAttribute('role','status');document.body.append(toast);}toast.textContent=text;toast.hidden=false;setTimeout(()=>{toast.hidden=true;},3000);}
|
||||
async function copy(text){
|
||||
try{await navigator.clipboard.writeText(text);status('کپی شد');}
|
||||
catch{
|
||||
const dialog=document.createElement('dialog');dialog.className='pay-manual-copy';dialog.setAttribute('aria-label','کپی اطلاعات');
|
||||
const title=document.createElement('h2');title.textContent='متن را انتخاب و کپی کنید';const area=document.createElement('textarea');area.readOnly=true;area.value=text;
|
||||
const select=document.createElement('button');select.className='pay-button';select.textContent='انتخاب متن';select.onclick=()=>{area.focus();area.select();};
|
||||
const close=document.createElement('button');close.className='pay-button secondary';close.textContent='بستن';close.onclick=()=>dialog.close();dialog.append(title,area,select,close);document.body.append(dialog);dialog.addEventListener('close',()=>dialog.remove());dialog.showModal();area.focus();area.select();
|
||||
}
|
||||
}
|
||||
document.querySelectorAll('[data-copy]').forEach(button=>button.addEventListener('click',()=>copy(button.dataset.copy)));
|
||||
const identifiers=document.querySelector('#identifier-accounts'),account=document.querySelector('#id_account'),deposit=document.querySelector('#id_deposit_id');
|
||||
if(identifiers && account && deposit){const ids=JSON.parse(identifiers.textContent).map(String);function update(){deposit.required=ids.includes(account.value);document.querySelector('#identifier-help').textContent=deposit.required?'این حساب شناسهدار است؛ شناسه واریز مشتری الزامی است.':'شناسه واریز در صورت اعلام بانک وارد شود.';}account.addEventListener('change',update);update();}
|
||||
const badge=document.querySelector('[data-payment-count]');
|
||||
if(badge){const update=async()=>{if(document.hidden)return;try{const response=await fetch(badge.dataset.countUrl,{credentials:'same-origin',cache:'no-store'});if(!response.ok)return;const data=await response.json();document.querySelectorAll('[data-payment-count]').forEach(node=>{node.textContent=String(data.pending_receipts);});}catch{}};document.addEventListener('visibilitychange',update);setInterval(update,60000);}
|
||||
})();
|
||||
@@ -0,0 +1,78 @@
|
||||
/* Local amount presentation: never convert rials to tomans for the server.
|
||||
Exact BigInt arithmetic; preserve fractional toman (one rial = one tenth toman).
|
||||
Shared with notice amounts; visible grouping does not replace server validation. */
|
||||
(() => {
|
||||
'use strict';
|
||||
const input=document.querySelector('#id_paid_amount, #id_amount');if(!input)return;
|
||||
input.inputMode='numeric';
|
||||
const words=document.querySelector('#amount-words');
|
||||
const small=['صفر','یک','دو','سه','چهار','پنج','شش','هفت','هشت','نه','ده','یازده','دوازده','سیزده','چهارده','پانزده','شانزده','هفده','هجده','نوزده'];
|
||||
const tens=['','','بیست','سی','چهل','پنجاه','شصت','هفتاد','هشتاد','نود'];
|
||||
const hundreds=['','صد','دویست','سیصد','چهارصد','پانصد','ششصد','هفتصد','هشتصد','نهصد'];
|
||||
const scales=['','هزار','میلیون','میلیارد','تریلیون','کوادریلیون'];
|
||||
const ascii=value=>value.replace(/[۰-۹]/g,digit=>String(digit.charCodeAt(0)-1776)).replace(/[٠-٩]/g,digit=>String(digit.charCodeAt(0)-1632));
|
||||
const digitCount=value=>(ascii(value).match(/\d/g) || []).length;
|
||||
const separators=/[,٬ \u00a0]/g;
|
||||
const grouped=value=>value.replace(/\B(?=(\d{3})+(?!\d))/g,'٬').replace(/\d/g,digit=>'۰۱۲۳۴۵۶۷۸۹'[digit]);
|
||||
function threeDigits(value){
|
||||
const parts=[];
|
||||
if(value>=100)parts.push(hundreds[Math.floor(value/100)]);
|
||||
const rest=value%100;
|
||||
if(rest && rest<20)parts.push(small[rest]);
|
||||
if(rest>=20){parts.push(tens[Math.floor(rest/10)]);if(rest%10)parts.push(small[rest%10]);}
|
||||
return parts.join(' و ');
|
||||
}
|
||||
function numberWords(value){
|
||||
if(value===0n)return 'صفر';
|
||||
const parts=[];let index=0;
|
||||
while(value>0n){
|
||||
const group=Number(value%1000n);
|
||||
if(group){const text=group===1 && index===1?'هزار':threeDigits(group)+(scales[index]?' '+scales[index]:'');parts.unshift(text);}
|
||||
value/=1000n;index++;
|
||||
}
|
||||
return parts.join(' و ');
|
||||
}
|
||||
function tomanWords(rials){
|
||||
const whole=rials/10n,fraction=rials%10n;
|
||||
if(!fraction)return numberWords(whole)+' تومان';
|
||||
return (whole?numberWords(whole)+' و ':'')+numberWords(fraction)+' دهم تومان';
|
||||
}
|
||||
function caretAfterDigits(value,count){
|
||||
if(!count)return 0;
|
||||
for(let index=0;index<value.length;index++)if(/\d/.test(ascii(value[index])) && --count===0)return index+1;
|
||||
return value.length;
|
||||
}
|
||||
function formatAmount(event){
|
||||
if(event?.isComposing)return;
|
||||
const raw=input.value,start=input.selectionStart ?? raw.length,end=input.selectionEnd ?? start;
|
||||
const normalized=ascii(raw).trim();
|
||||
const valid=/^\d+$/.test(normalized) || /^\d{1,3}(?:[,٬ \u00a0]\d{3})+$/.test(normalized);
|
||||
// Re-group after genuine digit edits/deletion, but never sanitize letters, signs or decimal input.
|
||||
const editing=event?.inputType?.startsWith('delete') || (event?.inputType==='insertText' && /^\d+$/.test(ascii(event.data || '')));
|
||||
if(!valid && !(editing && /^[\d,٬ \u00a0]+$/.test(normalized))){if(words)words.textContent='';return;}
|
||||
const digits=normalized.replace(separators,'').replace(/^0+(?=\d)/,'');
|
||||
if(!digits || digits.length>16 || BigInt(digits)>BigInt(Number.MAX_SAFE_INTEGER)){if(words)words.textContent='';return;}
|
||||
const leading=normalized.replace(separators,'').length-digits.length;
|
||||
const left=Math.max(0,digitCount(raw.slice(0,start))-leading),right=Math.max(0,digitCount(raw.slice(0,end))-leading);
|
||||
input.value=grouped(digits);
|
||||
input.setSelectionRange(caretAfterDigits(input.value,left),caretAfterDigits(input.value,right));
|
||||
if(words)words.textContent='معادل: '+tomanWords(BigInt(digits));
|
||||
}
|
||||
input.addEventListener('input',formatAmount);
|
||||
input.addEventListener('compositionend',()=>formatAmount());
|
||||
input.addEventListener('beforeinput',event=>{
|
||||
const start=input.selectionStart,end=input.selectionEnd;
|
||||
if(start!==end || event.isComposing)return;
|
||||
const backward=event.inputType==='deleteContentBackward',forward=event.inputType==='deleteContentForward';
|
||||
const adjacent=backward?start-1:start;
|
||||
if((!backward && !forward) || !/[٬,]/.test(input.value[adjacent] || ''))return;
|
||||
// Deleting a separator removes the adjacent digit, so grouping cannot trap the caret.
|
||||
const target=backward?adjacent-1:adjacent+1;
|
||||
const count=digitCount(input.value.slice(0,target));
|
||||
const raw=input.value.slice(0,target)+input.value.slice(target+1);
|
||||
event.preventDefault();input.value=ascii(raw).replace(separators,'');
|
||||
input.setSelectionRange(count,count);input.dispatchEvent(new InputEvent('input',{bubbles:true,inputType:event.inputType}));
|
||||
});
|
||||
input.addEventListener('blur',()=>formatAmount());
|
||||
formatAmount();
|
||||
})();
|
||||
@@ -0,0 +1,70 @@
|
||||
/* Server remains authoritative. Display Jalali, submit an ASCII Gregorian date-only.
|
||||
No dates, bank details or receipts are placed in localStorage/service-worker caches. */
|
||||
(() => {
|
||||
'use strict';
|
||||
const form=document.querySelector('#receipt-form');if(!form)return;
|
||||
const q=name=>form.elements.namedItem(name),button=document.querySelector('#submit-receipt');
|
||||
const notices=JSON.parse(document.querySelector('#notice-data').textContent);
|
||||
const day=q('paid_day'),month=q('paid_month'),year=q('paid_year');
|
||||
const digits=value=>String(value).replace(/[۰-۹]/g,x=>String(x.charCodeAt(0)-1776)).replace(/[٠-٩]/g,x=>String(x.charCodeAt(0)-1632));
|
||||
const formatter=new Intl.DateTimeFormat('en-US',{calendar:'persian',numberingSystem:'latn',timeZone:'Asia/Tehran',year:'numeric',month:'numeric',day:'numeric'});
|
||||
const parts=date=>Object.fromEntries(formatter.formatToParts(date).map(p=>[p.type,p.value]));
|
||||
function newYear(y){for(let d=18;d<=24;d++){const date=new Date(Date.UTC(y+621,2,d,12)),p=parts(date);if(Number(p.year)===y && Number(p.month)===1 && Number(p.day)===1)return date;}throw Error('تاریخ معتبر نیست.');}
|
||||
function dateValue(){
|
||||
const output=document.querySelector('#paid-date-iso');output.value='';
|
||||
if(!day.value || !month.value || !year.value)return '';
|
||||
try{const y=Number(year.value),m=Number(month.value),d=Number(day.value),offset=m<=6?(m-1)*31:186+(m-7)*30;const date=new Date(newYear(y).getTime()+(offset+d-1)*86400000),p=parts(date);if(Number(p.year)!==y || Number(p.month)!==m || Number(p.day)!==d)return '';output.value=date.toISOString().slice(0,10);return output.value;}catch{return '';}
|
||||
}
|
||||
function dateUpdate(){const iso=dateValue();document.querySelector('#date-preview').textContent=iso?'تاریخ انتخابشده: '+day.options[day.selectedIndex].text+' '+month.options[month.selectedIndex].text+' '+year.value:'روز، ماه و سال پرداخت را کامل انتخاب کنید.';}
|
||||
function monthUpdate(){
|
||||
let length=31;
|
||||
if(month.value && year.value){const m=Number(month.value),y=Number(year.value);length=m<=6?31:m<=11?30:(newYear(y+1)-newYear(y))/86400000===366?30:29;}
|
||||
[...day.options].forEach(option=>{option.disabled=Number(option.value)>length;});if(Number(day.value)>length)day.value='';dateUpdate();
|
||||
}
|
||||
[month,year].forEach(control=>control.addEventListener('change',monthUpdate));day.addEventListener('change',dateUpdate);
|
||||
document.querySelector('#date-today').addEventListener('click',()=>{const p=parts(new Date());year.value=p.year;month.value=p.month;monthUpdate();day.value=p.day;dateUpdate();day.focus();});monthUpdate();
|
||||
function sync(){
|
||||
const linked=q('receipt_mode').value==='linked';const select=q('notice_id');form.querySelector('[data-field=notice_id]').hidden=!linked;select.disabled=!linked;select.required=linked;
|
||||
if(!linked)select.value='';
|
||||
const notice=linked?notices.find(n=>n.id===select.value):null;const summary=document.querySelector('#selected-payment');summary.hidden=!notice;
|
||||
const summaryText=notice?'مبلغ اعلامشده: '+new Intl.NumberFormat('fa-IR').format(BigInt(notice.amount))+' ریال · شناسه واریز: '+(notice.deposit || 'اعلام نشده')+(notice.invoice?' · فاکتور: '+notice.invoice:''):'';
|
||||
const title=summary.querySelector('[data-selected-notice-title]');
|
||||
if(title){title.textContent=notice?notice.subject:'';summary.querySelector('[data-selected-notice-summary]').textContent=summaryText;summary.querySelector('[data-selected-notice-link]').href=notice?notice.url:'#';}else summary.textContent=summaryText;
|
||||
const finalSummary=document.querySelector('#receipt-summary');if(finalSummary)finalSummary.textContent=notice?'این فیش برای «'+notice.subject+'» ارسال میشود.':linked?'اعلامیه مرتبط را انتخاب کنید.':'این فیش به صورت واریز مستقل برای بررسی ارسال میشود.';
|
||||
const depositWarning=summary.querySelector('[data-selected-deposit-warning]');
|
||||
depositWarning.hidden=!notice?.deposit;
|
||||
q('deposit_id').classList.toggle('pay-deposit-emphasis',Boolean(notice?.deposit));
|
||||
q('deposit_id').required=false;
|
||||
}
|
||||
form.querySelectorAll('[name=receipt_mode]').forEach(control=>control.addEventListener('change',sync));q('notice_id').addEventListener('change',()=>{const notice=notices.find(n=>n.id===q('notice_id').value);if(notice){q('destination_account').value=String(notice.account);q('deposit_id').value=notice.deposit;}sync();});q('destination_account').addEventListener('change',sync);sync();
|
||||
const fields=[...form.querySelectorAll('[data-field]')];
|
||||
fields.forEach(field=>{const controls=field.querySelectorAll('input,select,textarea');controls.forEach(control=>{control.setAttribute('aria-describedby','error-'+field.dataset.field);if(field.querySelector('.field-error').textContent.trim())control.setAttribute('aria-invalid','true');});});
|
||||
let busy=false;
|
||||
function showErrors(errors){
|
||||
fields.forEach(field=>{const name=field.dataset.field,message=errors.get(name)||'',error=field.querySelector('.field-error');error.textContent=message;error.hidden=!message;field.querySelectorAll('input,select,textarea').forEach(control=>control.setAttribute('aria-invalid',String(Boolean(message))));});
|
||||
const summary=document.querySelector('#receipt-errors'),list=summary.querySelector('ul');list.replaceChildren();errors.forEach((message,name)=>{const item=document.createElement('li'),link=document.createElement('a');link.href='#id_'+name;link.textContent=message;link.onclick=event=>{event.preventDefault();q(name).focus();q(name).scrollIntoView({block:'center'});};item.append(link);list.append(item);});summary.hidden=!errors.size;
|
||||
if(errors.size){const first=q(errors.keys().next().value);first.focus();first.scrollIntoView({block:'center'});}
|
||||
}
|
||||
async function validateImage(file){
|
||||
if(!file)return 'تصویر فیش واریز را انتخاب کنید.';
|
||||
if(!file.size || file.size>Number(form.dataset.maxBytes))return 'حجم تصویر بیش از حد مجاز است یا تصویر خالی است.';
|
||||
const extension=file.name.split('.').pop().toLowerCase();if(!['jpg','jpeg','png','gif','webp'].includes(extension))return 'فقط تصویر JPG، PNG، GIF یا WEBP انتخاب کنید.';
|
||||
const readable=await new Promise(resolve=>{const picture=new Image(),url=URL.createObjectURL(file);picture.onload=()=>{URL.revokeObjectURL(url);resolve(picture.naturalWidth*picture.naturalHeight<=25000000);};picture.onerror=()=>{URL.revokeObjectURL(url);resolve(false);};picture.src=url;});return readable?'':'تصویر باز نمیشود یا ابعاد آن بیش از حد مجاز است.';
|
||||
}
|
||||
let revision=0;form.addEventListener('input',()=>revision++);form.addEventListener('change',()=>revision++);
|
||||
form.addEventListener('submit',async event=>{
|
||||
event.preventDefault();if(busy)return;busy=true;button.disabled=true;const version=revision,errors=new Map();
|
||||
try{
|
||||
const value=digits(q('paid_amount').value).trim();if(!/^\d+$/.test(value) && !/^\d{1,3}(?:[,٬ ]\d{3})+$/.test(value))errors.set('paid_amount','مبلغ را به ریال و با عدد صحیح وارد کنید.');else{const amount=BigInt(value.replace(/[,٬ ]/g,''));if(amount<=0n || amount>9007199254740991n)errors.set('paid_amount','مبلغ باید مثبت و در محدوده مجاز باشد.');}
|
||||
if(q('receipt_mode').value==='linked' && !q('notice_id').value)errors.set('notice_id','اعلامیه را انتخاب کنید یا واریز مستقل را بزنید.');
|
||||
if(!q('destination_account').value)errors.set('destination_account','حساب مقصد را انتخاب کنید.');
|
||||
const iso=dateValue();if(!iso)errors.set('paid_day','تاریخ را کامل و معتبر انتخاب کنید؛ روز، ماه و سال لازماند.');
|
||||
else{const now=Object.fromEntries(new Intl.DateTimeFormat('en-US',{timeZone:'Asia/Tehran',year:'numeric',month:'2-digit',day:'2-digit'}).formatToParts(new Date()).map(p=>[p.type,p.value]));if(iso>[now.year,now.month,now.day].join('-'))errors.set('paid_day','تاریخ واریز نمیتواند بعد از امروز باشد.');}
|
||||
const imageError=await validateImage(q('receipt_image').files[0]);if(imageError)errors.set('receipt_image',imageError);
|
||||
if(version!==revision){document.querySelector('#form-status').textContent='اطلاعات تغییر کرده است؛ دوباره ارسال را بزنید.';return;}
|
||||
showErrors(errors);if(errors.size)return;
|
||||
button.textContent='در حال ثبت…';HTMLFormElement.prototype.submit.call(form);return;
|
||||
}finally{busy=false;if(button.textContent!=='در حال ثبت…')button.disabled=false;}
|
||||
});
|
||||
if(document.querySelector('#receipt-errors').hidden===false){const field=form.querySelector('[aria-invalid=true]');if(field){field.focus();field.scrollIntoView({block:'center'});}}
|
||||
})();
|
||||
@@ -0,0 +1,21 @@
|
||||
const confirmationForm = document.querySelector('[data-magic-confirmation]');
|
||||
if (confirmationForm) {
|
||||
confirmationForm.addEventListener('submit', (event) => {
|
||||
const button = confirmationForm.querySelector('button[type="submit"]');
|
||||
if (button.disabled) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
button.disabled = true;
|
||||
button.setAttribute('aria-busy', 'true');
|
||||
button.textContent = 'در حال باز کردن تیکت…';
|
||||
confirmationForm.querySelector('[role="status"]').textContent = button.textContent;
|
||||
});
|
||||
window.addEventListener('pageshow', () => {
|
||||
const button = confirmationForm.querySelector('button[type="submit"]');
|
||||
button.disabled = false;
|
||||
button.removeAttribute('aria-busy');
|
||||
button.textContent = 'خروج از حساب و مشاهده تیکت';
|
||||
confirmationForm.querySelector('[role="status"]').textContent = '';
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
(() => {
|
||||
const events = document.querySelector('#id_event_type');
|
||||
const tags = document.querySelector('#sms-allowed-tags');
|
||||
const data = document.querySelector('#sms-tag-map');
|
||||
if (!events || !tags || !data) return;
|
||||
const mapping = JSON.parse(data.textContent);
|
||||
function showAllowedTags() {
|
||||
tags.replaceChildren(...(mapping[events.value] || []).map(tag => {
|
||||
const code = document.createElement('code');
|
||||
code.textContent = '{' + tag + '}';
|
||||
return code;
|
||||
}));
|
||||
}
|
||||
events.addEventListener('change', showAllowedTags);
|
||||
showAllowedTags();
|
||||
})();
|
||||
Reference in new issue
Block a user