/* Browser-only design prototype: submit is ALWAYS prevented, nothing uploaded. Production must validate ownership, amount, date and real file content again. Limits here follow bootstrap IMAGE defaults, not a read of live AttachmentPolicy. */ (() => { 'use strict'; const form=document.querySelector('#receipt-form'); if(!form)return; const q=selector=>form.querySelector(selector); const amount=q('#paid-amount'),file=q('#receipt-file'),button=q('#submit-receipt'); const summary=q('#receipt-errors'),status=q('#receipt-validation-status'); const groups={ notice:{controls:[q('#notice-select')],error:q('#notice-error')}, amount:{controls:[amount],error:q('#amount-error')}, destination:{controls:[q('#destination-account')],error:q('#destination-error')}, date:{controls:[q('#receipt-day'),q('#receipt-month'),q('#receipt-year')],error:q('#date-error')}, file:{controls:[file],error:q('#file-error')} }; const messages=new Map(); const touched=new Set(); const imageChecks=new WeakMap(); let submitted=false,busy=false,revision=0,fileRevision=0; function focusField(control){ const visible=control===file?q('.file-picker-label'):control; visible.scrollIntoView({block:'center',behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'auto':'smooth'}); control.focus({preventScroll:true}); } function firstControl(key){ const controls=groups[key].controls; return controls.find(control=>!control.value) || controls[0]; } function renderSummary(){ const list=summary.querySelector('ul');list.replaceChildren(); messages.forEach((message,key)=>{ const item=document.createElement('li'),link=document.createElement('a'); link.href='#'+firstControl(key).id;link.textContent=message; link.addEventListener('click',event=>{event.preventDefault();focusField(firstControl(key));}); item.append(link);list.append(item); }); summary.hidden=!submitted || messages.size===0; } function showError(key,message){ const group=groups[key];group.error.textContent=message;group.error.hidden=!message; group.controls.forEach(control=>control.setAttribute('aria-invalid',String(Boolean(message)))); if(message)messages.set(key,message);else messages.delete(key); renderSummary(); } function normalizeDigits(value){ return value.replace(/[۰-۹]/g,digit=>String(digit.charCodeAt(0)-1776)).replace(/[٠-٩]/g,digit=>String(digit.charCodeAt(0)-1632)).trim(); } function amountError(){ q('#paid-amount-value').value=''; const value=normalizeDigits(amount.value); if(!value)return 'مبلغ واریز را به ریال وارد کنید.'; if(value.startsWith('-'))return 'مبلغ باید بیشتر از صفر باشد.'; if(/[.٫]/.test(value))return 'مبلغ را به ریال و بدون اعشار وارد کنید.'; if(!/^\d+$/.test(value) && !/^\d{1,3}(?:[,٬ \u00a0]\d{3})+$/.test(value))return 'مبلغ را فقط با عدد بنویسید؛ مثلاً ۲۸۰۰۰۰۰۰۰.'; const digits=value.replace(/[,٬ \u00a0]/g,'').replace(/^0+(?=\d)/,''); if(digits==='0')return 'مبلغ باید بیشتر از صفر باشد.'; // Avoid imprecise conversion: payload remains an integer string. if(digits.length>16 || BigInt(digits)>BigInt(Number.MAX_SAFE_INTEGER))return 'مبلغ بسیار بزرگ است؛ عدد روی فیش را دوباره بررسی کنید.'; q('#paid-amount-value').value=digits;return ''; } function noticeError(){ if(q('input[name="receipt-mode"]:checked').value!=='linked')return ''; const selected=q('#notice-select').value; if(!(window.chaharsuPaymentNotices || []).some(notice=>notice.id===selected))return 'واریز مرتبط را انتخاب کنید؛ اگر اعلامیه ندارید، واریز مستقل را بزنید.'; return ''; } function destinationError(){ const selected=q('#destination-account').value; if(!(window.chaharsuCompanyAccounts || []).some(account=>account.id===selected))return 'حساب مقصد را انتخاب کنید؛ مبلغ را به کدام حساب شرکت واریز کرده‌اید؟'; return ''; } function dateError(){ form.dispatchEvent(new Event('receipt-date:validate')); const controls=groups.date.controls; if(controls.some(control=>!control.value))return 'تاریخ را کامل کنید؛ روز، ماه و سال لازم‌اند.'; if(controls.some(control=>control.disabled || !control.checkValidity()))return 'تاریخ معتبر انتخاب کنید؛ روز باید در ماه و سال انتخاب‌شده وجود داشته باشد.'; const value=q('#receipt-date-value').value; if(!/^\d{4}-\d{2}-\d{2}$/.test(value))return 'تاریخ معتبر انتخاب کنید؛ روز، ماه و سال را دوباره بررسی کنید.'; const todayParts=new Intl.DateTimeFormat('en-US',{timeZone:'Asia/Tehran',year:'numeric',month:'2-digit',day:'2-digit'}).formatToParts(new Date()); const today=Object.fromEntries(todayParts.map(part=>[part.type,part.value])); if(value>[today.year,today.month,today.day].join('-'))return 'تاریخ واریز نمی‌تواند بعد از امروز باشد.'; return ''; } function detectedType(bytes){ const text=(start,end)=>String.fromCharCode(...bytes.slice(start,end)); if(bytes[0]===255 && bytes[1]===216 && bytes[2]===255)return 'image/jpeg'; if([...bytes.slice(0,8)].join(',')==='137,80,78,71,13,10,26,10')return 'image/png'; if(['GIF87a','GIF89a'].includes(text(0,6)))return 'image/gif'; if(text(0,4)==='RIFF' && text(8,12)==='WEBP')return 'image/webp'; return ''; } async function checkImage(selected){ const max=Number(file.dataset.maxBytes); if(!selected.size)return 'تصویر انتخاب‌شده خالی است؛ تصویر دیگری انتخاب کنید.'; if(selected.size>max)return 'حجم تصویر بیشتر از ۱۰ مگابایت است؛ تصویر کم‌حجم‌تری انتخاب کنید.'; const allowed={jpg:'image/jpeg',jpeg:'image/jpeg',png:'image/png',gif:'image/gif',webp:'image/webp'}; const extension=selected.name.split('.').pop().toLowerCase(); if(!allowed[extension])return 'فقط تصویر JPG، PNG، GIF یا WEBP انتخاب کنید.'; const type=detectedType(new Uint8Array(await selected.slice(0,16).arrayBuffer())); if(type!==allowed[extension] || (selected.type && selected.type!==type))return 'محتوای فایل با نوع تصویر سازگار نیست؛ تصویر اصلی فیش را انتخاب کنید.'; const readable=await new Promise(resolve=>{ const image=new Image(),url=URL.createObjectURL(selected); image.onload=()=>{URL.revokeObjectURL(url);resolve(image.naturalWidth>0 && image.naturalHeight>0);}; image.onerror=()=>{URL.revokeObjectURL(url);resolve(false);};image.src=url; }); return readable?'':'تصویر باز نمی‌شود یا خراب است؛ تصویر دیگری انتخاب کنید.'; } async function fileError(){ const selected=file.files[0]; if(!selected)return 'تصویر فیش واریز را انتخاب کنید.'; if(!imageChecks.has(selected))imageChecks.set(selected,checkImage(selected).catch(()=> 'تصویر قابل بررسی نیست؛ دوباره آن را انتخاب کنید.')); return imageChecks.get(selected); } const validators={notice:noticeError,amount:amountError,destination:destinationError,date:dateError,file:fileError}; function refresh(key){ status.textContent=''; if(key==='file')return refreshFile(); if(submitted || touched.has(key))showError(key,validators[key]()); } async function refreshFile(){ const token=++fileRevision;touched.add('file'); const message=await fileError();if(token===fileRevision)showError('file',message); } form.addEventListener('input',()=>{revision++;status.textContent='';}); form.addEventListener('change',()=>{revision++;status.textContent='';}); amount.addEventListener('blur',()=>{touched.add('amount');refresh('amount');}); amount.addEventListener('input',()=>{q('#paid-amount-value').value='';refresh('amount');}); groups.date.controls.forEach(control=>control.addEventListener('change',()=>refresh('date'))); q('#receipt-date-today').addEventListener('click',()=>{revision++;refresh('date');}); q('#notice-select').addEventListener('change',()=>refresh('notice')); q('#destination-account').addEventListener('change',()=>refresh('destination')); q('#destination-account').addEventListener('blur',()=>{touched.add('destination');refresh('destination');}); form.querySelectorAll('input[name="receipt-mode"]').forEach(control=>control.addEventListener('change',()=>refresh('notice'))); file.addEventListener('change',()=>refresh('file')); q('#clear-file').addEventListener('click',()=>{revision++;refresh('file');}); form.addEventListener('submit',async event=>{ event.preventDefault();if(busy)return; submitted=true;busy=true;button.disabled=true;button.textContent='در حال بررسی…';status.textContent=''; const current=revision; try{ const results=[['notice',noticeError()],['amount',amountError()],['destination',destinationError()],['date',dateError()],['file',await fileError()]]; if(current!==revision){status.textContent='اطلاعات فرم تغییر کرده است؛ دوباره دکمهٔ بررسی را بزنید.';return;} messages.clear();results.forEach(([key,message])=>showError(key,message)); if(messages.size){focusField(firstControl(messages.keys().next().value));return;} status.textContent='اطلاعات فرم کامل و معتبر است. این پیش‌نمایش است؛ فیش ارسال یا ذخیره نشده است.'; }finally{busy=false;button.disabled=false;button.textContent='بررسی و ادامه';} }); // Enable only when prevention and validation handlers are installed; no accidental native submit. button.disabled=false; })();