Files
customerTicket/design-prototypes/chaharsu/receipt-validation.js
T
2026-10-05 12:36:58 +03:30

153 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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;
})();