design-prototypes/chaharsu

This commit is contained in:
javad committed 2026-10-05 12:36:58 +03:30
1 parent 79fe1cddbb
commit 2ae79b1ca4
27 files changed
+2104

No files matched your search

@@ -0,0 +1,152 @@
/* 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;
})();