حساب‌یار

سامانه مدیریت مالی و صرافی

برای ورود، ابتدا مدیر کل باید حساب کاربری شما را در سامانه ایجاد کرده باشد.

,decimal_places:2}, {code:'AED',name_fa:'درهم امارات',name_en:'UAE Dirham',symbol:'د.إ',decimal_places:2}, {code:'PKR',name_fa:'روپیه پاکستان',name_en:'Pakistani Rupee',symbol:'₨',decimal_places:2}, {code:'IRT',name_fa:'تومان',name_en:'Iranian Toman',symbol:'تومان',decimal_places:0} ]; try{ let current=currencies.slice(),created=[]; for(const item of defaults){ let currency=current.find(x=>x.code.toUpperCase()===item.code); if(!currency){ const {data,error}=await client.rpc('create_currency',{p_code:item.code,p_name_fa:item.name_fa,p_name_en:item.name_en,p_symbol:item.symbol,p_decimal_places:item.decimal_places}); if(error)throw new Error('ایجاد '+item.code+' ناموفق بود: '+error.message); created.push(item.code); const {data:loaded,error:loadError}=await client.from('currencies').select('id,code,name_fa,name_en,symbol,decimal_places,is_active').eq('id',data).single(); if(loadError)throw new Error('دریافت اطلاعات '+item.code+' ناموفق بود: '+loadError.message); currency=loaded;current.push(currency); } if(currency.id!==baseCurrencyId){ for(const branchItem of branches){ const {error}=await client.rpc('ensure_currency_inventory_accounts',{p_branch_id:branchItem.id,p_currency_id:currency.id}); if(error)throw new Error('ساخت حساب '+item.code+' برای شعبه '+branchItem.name+' ناموفق بود: '+error.message); } } } await renderCurrencies($('currencyBranchFilter').value); status.textContent=(created.length?'ارزهای جدید '+created.join('، ')+' ایجاد شدند. ':'هر سه ارز از قبل تعریف شده بودند. ')+'حساب‌های موجودی و سود و زیان برای شعب فعال آماده شدند.'; status.style.color='var(--good)'; }catch(err){status.textContent='عملیات کامل نشد: '+(err?.message||String(err));status.style.color='var(--danger)'} finally{const currentButton=$('addDefaultCurrencies');if(currentButton)currentButton.disabled=false} }); renderRows(); $('currencyCreateForm').addEventListener('submit',async e=>{ e.preventDefault(); const msg=$('currencyMessage');msg.textContent='';msg.style.color=''; const branch=$('currencyBranch').value; const code=$('currencyCode').value.trim().toUpperCase(); const nameFa=$('currencyNameFa').value.trim(); const nameEn=$('currencyNameEn').value.trim(); const symbol=$('currencySymbol').value.trim(); const decimals=Number($('currencyDecimals').value); if(!branch||!code||!nameFa||!Number.isInteger(decimals)||decimals<0||decimals>6){msg.textContent='اطلاعات ارز یا شعبه معتبر نیست.';return} const btn=$('currencyCreateBtn');btn.disabled=true; try{ const {data,error}=await client.rpc('create_currency',{p_code:code,p_name_fa:nameFa,p_name_en:nameEn||null,p_symbol:symbol||null,p_decimal_places:decimals}); if(error)throw error; const {error:mappingError}=await client.rpc('ensure_currency_inventory_accounts',{p_branch_id:branch,p_currency_id:data}); if(mappingError){ await renderCurrencies(branch); $('currencyMessage').textContent='ارز '+code+' ایجاد شد، اما ساخت حساب‌های شعبه ناموفق بود. از فهرست ارزها دوباره عملیات ایجاد حساب را اجرا کنید. '+mappingError.message; $('currencyMessage').style.color='var(--danger)'; }else{ $('currencyCreateForm').reset();$('currencyDecimals').value='2'; await renderCurrencies(branch); $('currencyMessage').textContent='ارز '+code+' و حساب‌های موجودی، سود و زیان شعبه با موفقیت ایجاد شدند.'; $('currencyMessage').style.color='var(--good)'; } }catch(err){ msg.textContent='ثبت ارز ناموفق بود: '+(err?.message||String(err)); }finally{ const currentBtn=$('currencyCreateBtn');if(currentBtn)currentBtn.disabled=false; } }); } function renderSettings(){ $('moduleHeading').textContent='تنظیمات برنامه'; $('moduleDescription').textContent='زبان و قالب نمایش در پروفایل کاربر ذخیره می‌شوند و در دستگاه‌های دیگر نیز بازیابی خواهند شد. تنظیمات مالی مرکزی فقط از مسیرهای امن مدیرکل تغییر می‌کنند.'; $('moduleCards').innerHTML=`

زبان و جهت نمایش

زبان در حساب کاربری ذخیره می‌شود و پس از ورود از دستگاه‌های دیگر نیز بازیابی خواهد شد.

ظاهر برنامه

قالب در حساب کاربری ذخیره می‌شود؛ در حالت دستگاه، ظاهر سیستم‌عامل یا مرورگر اعمال خواهد شد.

تقویم پیش‌فرض برنامه

این گزینه تنظیم مرکزی برنامه است و فقط مدیرکل می‌تواند آن را تغییر دهد. تاریخ‌های ورودی در فرم‌ها فعلاً از کنترل تاریخ مرورگر استفاده می‌کنند.

سیاست گزارش سود و زیان

مدیر و مدیرکل می‌توانند این سیاست کلی را تغییر دهند. در حالت خاموش، سود تحقق‌یافته ارز از درآمد گزارش کنار گذاشته می‌شود؛ زیان‌های تحقق‌یافته همچنان در هزینه‌ها محاسبه می‌شوند.

ارز پایه و اطلاعات مالی

در حال دریافت ارز پایه...

تغییر ارز پایه می‌تواند بر گزارش‌ها و ارزش‌گذاری اثر بگذارد؛ این عملیات تا زمان تکمیل گردش تبدیل و ارزش‌گذاری، از این صفحه قابل تغییر نیست.

پشتیبان‌گیری

نسخهٔ پشتیبان کامل باید شامل پایگاه داده، فایل‌ها، تنظیمات و امکان بازیابی آزمایش‌شده باشد. این قابلیت هنوز پیاده‌سازی و آزمون نشده است؛ بنابراین دکمهٔ خروجی یا بازیابی ساختگی نمایش داده نمی‌شود.

`; $('settingsLanguage').value=lang; $('settingsThemeSelect').value=theme; $('settingsLanguageSave').onclick=async()=>{ lang=$('settingsLanguage').value;localStorage.setItem('hesabyar_lang',lang);if(profile?.id)localStorage.setItem('hesabyar_lang_'+profile.id,lang); document.documentElement.lang=lang;document.documentElement.dir=lang==='en'?'ltr':'rtl';$('language').value=lang; renderNav();$('pageTitle').textContent=menuText(menu.find(x=>x[0]==='settings')); $('settingsLanguage').value=lang; try{await saveProfilePreferences({preferred_language:lang});$('settingsSaveMsg').textContent='زبان در حساب کاربری ذخیره شد.'}catch(err){$('settingsSaveMsg').textContent='ذخیره زبان در سرور ناموفق بود: '+err.message} }; $('settingsThemeSave').onclick=async()=>{const nextTheme=$('settingsThemeSelect').value;if(!['light','dark','system'].includes(nextTheme)){ $('settingsSaveMsg').textContent='قالب انتخابی معتبر نیست.';return }const previousTheme=theme;theme=nextTheme;applyTheme();$('settingsSaveMsg').textContent='در حال ذخیره قالب...';try{await saveProfilePreferences({theme});$('settingsSaveMsg').textContent='قالب در حساب کاربری ذخیره شد.'}catch(err){theme=previousTheme;applyTheme();$('settingsThemeSelect').value=theme;$('settingsSaveMsg').textContent='ذخیره قالب در سرور ناموفق بود: '+err.message}}; client.from('app_settings').select('base_currency_id,default_calendar,include_fx_gain_in_profit').eq('id',true).maybeSingle().then(async({data,error})=>{ if(error){ $('settingsBaseCurrency').textContent='دریافت تنظیمات مرکزی ناموفق بود: '+error.message; $('settingsCalendarMessage').textContent='دریافت تقویم پیش‌فرض ناموفق بود.'; $('settingsPnlMessage').textContent='دریافت سیاست گزارش ناموفق بود.'; return; } if(!data){$('settingsBaseCurrency').textContent='تنظیمات مرکزی پیدا نشد.';return} if(!data.base_currency_id){$('settingsBaseCurrency').textContent='ارز پایه تعیین نشده است.'} else{ const {data:c,error:ce}=await client.from('currencies').select('code,name_fa,name_en').eq('id',data.base_currency_id).maybeSingle(); $('settingsBaseCurrency').textContent=ce?'دریافت مشخصات ارز پایه ناموفق بود.':('ارز پایه فعلی: '+(c?.code||'—')+' — '+(lang==='en'?c?.name_en:c?.name_fa||c?.name_en||'')); } const currentCalendar=['jalali','gregorian'].includes(data.default_calendar)?data.default_calendar:'jalali'; calendar=currentCalendar; $('settingsCalendarSelect').value=currentCalendar; $('settingsIncludeFxGain').checked=!!data.include_fx_gain_in_profit; const isSuper=profile?.role==='super_admin'; const canManagePnl=['super_admin','manager'].includes(profile?.role); $('settingsCalendarSelect').disabled=!isSuper; $('settingsCalendarSave').disabled=!isSuper; $('settingsPnlSave').disabled=!canManagePnl; $('settingsIncludeFxGain').disabled=!canManagePnl; $('settingsCalendarMessage').textContent=isSuper?'تقویم فعلی از تنظیمات مرکزی خوانده شد.':'نمایش تقویم فقط برای مدیرکل قابل تغییر است.'; $('settingsPnlMessage').textContent=canManagePnl?'سیاست فعلی گزارش از تنظیمات مرکزی خوانده شد.':'مشاهده مجاز است؛ تغییر سیاست فقط برای مدیر و مدیرکل مجاز است.'; }); $('settingsCalendarSave').onclick=async()=>{ const next=$('settingsCalendarSelect').value,btn=$('settingsCalendarSave'); if(profile?.role!=='super_admin'){ $('settingsCalendarMessage').textContent='فقط مدیرکل می‌تواند تقویم پیش‌فرض را تغییر دهد.';return} btn.disabled=true;$('settingsCalendarMessage').textContent='در حال ذخیره تقویم...'; try{ const {error}=await client.rpc('update_reporting_preferences',{p_default_calendar:next,p_include_fx_gain_in_profit:$('settingsIncludeFxGain').checked}); if(error)throw error; calendar=next; $('settingsCalendarMessage').textContent='تقویم پیش‌فرض ذخیره شد.'; }catch(err){$('settingsCalendarMessage').textContent='ذخیره تقویم ناموفق بود: '+(err?.message||String(err))} finally{btn.disabled=false} }; $('settingsPnlSave').onclick=async()=>{ const btn=$('settingsPnlSave'),next=$('settingsCalendarSelect').value; if(!['super_admin','manager'].includes(profile?.role)){$('settingsPnlMessage').textContent='مجوز تغییر سیاست گزارش را ندارید.';return} btn.disabled=true;$('settingsPnlMessage').textContent='در حال ذخیره سیاست گزارش...'; try{ const {error}=await client.rpc('update_reporting_preferences',{p_default_calendar:next,p_include_fx_gain_in_profit:$('settingsIncludeFxGain').checked}); if(error)throw error; $('settingsPnlMessage').textContent='سیاست سود و زیان ذخیره شد؛ گزارش‌ها از این پس با همین تنظیم محاسبه می‌شوند.'; }catch(err){$('settingsPnlMessage').textContent='ذخیره سیاست گزارش ناموفق بود: '+(err?.message||String(err))} finally{btn.disabled=false} }; } async function renderUsers(){ if(profile?.role!=='super_admin'){$('moduleHeading').textContent='مدیریت کاربران';$('moduleDescription').textContent='فقط مدیرکل به این بخش دسترسی دارد.';$('moduleCards').replaceChildren();return} $('moduleHeading').textContent='کاربران و سطح دسترسی'; $('moduleDescription').textContent='ساخت کاربر، تعیین نقش و مدیریت دسترسی شعبه. ایجاد کاربر فقط از مسیر تابع امن مدیرکل انجام می‌شود.'; $('moduleCards').innerHTML=`

ایجاد کاربر جدید

ایجاد حساب از طریق سرویس امن سرور انجام می‌شود. مدیرکل باید مالکیت ایمیل را پیش از ساخت حساب بررسی کند؛ حساب پس از ایجاد با رمز اولیه قابل ورود است. رمز عبور در فهرست کاربران نمایش داده نمی‌شود.

نامنام کاربریایمیلنقششعبه‌هاوضعیتعملیات
`; let branches=[],users=[],branchMap=new Map(),editingId=null; const roleLabel={super_admin:'مدیر کل',manager:'مدیر',accountant:'حسابدار',employee:'کارمند'}; const branchSelect=$('usrBranches'); function selectedBranches(){return Array.from(branchSelect.selectedOptions).map(o=>o.value)} function setSelectedBranches(ids){for(const o of branchSelect.options)o.selected=ids.includes(o.value)} function resetForm(){ editingId=null;$('usrForm').reset();$('usrUsername').disabled=false;$('usrEmail').disabled=false;$('usrRole').innerHTML='';$('usrRole').value='manager';$('usrActive').value='true';$('usrPasswordWrap').classList.remove('hidden');$('usrPassword').required=true;$('usrActiveWrap').classList.add('hidden');$('usrCancel').classList.add('hidden');$('usrSubmit').textContent='ایجاد کاربر';$('usrFormTitle').textContent='ایجاد کاربر جدید';$('usrRole').innerHTML='';setSelectedBranches([]); } function renderRows(){ const body=$('usrRows');body.replaceChildren(); if(!users.length){const tr=document.createElement('tr'),td=document.createElement('td');td.colSpan=7;td.textContent='کاربری یافت نشد.';tr.append(td);body.append(tr);return} for(const u of users){ const tr=document.createElement('tr'),branchNames=(branchMap.get(u.id)||[]).map(x=>x.branches?.name||branches.find(b=>b.id===x.branch_id)?.name||'').filter(Boolean).join('، '); for(const val of [u.full_name||'—',u.username||'—',u.email||'—',roleLabel[u.role]||u.role,branchNames||'بدون شعبه',u.is_active?'فعال':'غیرفعال']){ const td=document.createElement('td');td.textContent=val;tr.append(td) } const td=document.createElement('td'),btn=document.createElement('button');btn.type='button';btn.className='btn ghost';btn.textContent='ویرایش';btn.addEventListener('click',()=>editUser(u));td.append(btn);tr.append(td);body.append(tr) } } function editUser(u){ editingId=u.id;$('usrFormTitle').textContent='ویرایش کاربر: '+(u.full_name||u.email);$('usrName').value=u.full_name||'';$('usrUsername').value=u.username||'';$('usrUsername').disabled=true;$('usrEmail').value=u.email||'';$('usrEmail').disabled=true; $('usrRole').innerHTML='';$('usrRole').value=u.role; $('usrPasswordWrap').classList.add('hidden');$('usrPassword').required=false;$('usrActiveWrap').classList.remove('hidden');$('usrActive').value=String(u.is_active);$('usrCancel').classList.remove('hidden');$('usrSubmit').textContent='ذخیره تغییرات'; setSelectedBranches((branchMap.get(u.id)||[]).map(x=>x.branch_id));$('usrMessage').textContent='';$('usrMessage').style.color='var(--bad)';$('usrForm').scrollIntoView({behavior:'smooth',block:'start'}); } async function loadUsers(){ $('usrRows').innerHTML='در حال دریافت...'; const [p,ub]=await Promise.all([ client.from('profiles').select('id,username,email,full_name,role,is_active,created_at').order('created_at',{ascending:true}).limit(1000), client.from('user_branches').select('user_id,branch_id,branches(name)') ]); const err=p.error||ub.error;if(err){$('usrRows').replaceChildren();const tr=document.createElement('tr'),td=document.createElement('td');td.colSpan=7;td.textContent='دریافت کاربران ناموفق بود: '+err.message;tr.append(td);$('usrRows').append(tr);return} users=p.data||[];branchMap=new Map(); for(const x of ub.data||[]){const list=branchMap.get(x.user_id)||[];list.push(x);branchMap.set(x.user_id,list)} renderRows(); } const b=await client.from('branches').select('id,name').eq('is_active',true).order('name'); if(b.error){$('usrMessage').textContent='دریافت شعبه‌ها ناموفق بود: '+b.error.message;return} branches=b.data||[];branchSelect.replaceChildren(); for(const x of branches){const o=document.createElement('option');o.value=x.id;o.textContent=x.name;branchSelect.append(o)} $('usrCancel').onclick=()=>{resetForm();$('usrMessage').textContent=''}; $('usrRefresh').onclick=loadUsers; $('usrForm').onsubmit=async e=>{ e.preventDefault();const msg=$('usrMessage');msg.textContent='';msg.style.color='var(--bad)'; const chosen=selectedBranches(); if(!editingId&&!chosen.length){msg.textContent='برای کاربر جدید دست‌کم یک شعبه انتخاب کنید.';return} const btn=e.submitter||$('usrForm').querySelector('button[type="submit"]');if(btn)btn.disabled=true; try{ if(editingId){ const u=users.find(x=>x.id===editingId),args={p_user_id:editingId,p_full_name:$('usrName').value.trim(),p_role:$('usrRole').value,p_is_active:$('usrActive').value==='true'}; const updated=await client.rpc('admin_update_user',args); if(updated.error){btn.disabled=false;msg.textContent='ویرایش کاربر ناموفق بود: '+updated.error.message;return} const current=(branchMap.get(editingId)||[]).map(x=>x.branch_id),grant=chosen.filter(id=>!current.includes(id)),revoke=current.filter(id=>!chosen.includes(id)); for(const id of [...grant.map(x=>({id:x,granted:true})),...revoke.map(x=>({id:x,granted:false}))]){ const res=await client.rpc('admin_set_user_branch',{p_user_id:editingId,p_branch_id:id.id,p_granted:id.granted}); if(res.error){btn.disabled=false;msg.textContent='مشخصات ذخیره شد اما تغییر دسترسی شعبه کامل نشد: '+res.error.message;await loadUsers();return} } btn.disabled=false;msg.style.color='var(--good)';msg.textContent='مشخصات و دسترسی شعبه‌های کاربر به‌روزرسانی شد.';resetForm();await loadUsers(); }else{ const body={username:$('usrUsername').value.trim(),email:$('usrEmail').value.trim(),full_name:$('usrName').value.trim(),password:$('usrPassword').value,role:$('usrRole').value}; const created=await client.functions.invoke('admin-create-user',{body}); if(created.error||!created.data?.id){btn.disabled=false;msg.textContent='ساخت کاربر ناموفق بود: '+(created.error?.message||created.data?.error||'پاسخ سرویس معتبر نیست.');return} let assignmentError=null; for(const id of chosen){const res=await client.rpc('admin_set_user_branch',{p_user_id:created.data.id,p_branch_id:id,p_granted:true});if(res.error){assignmentError=res.error;break}} btn.disabled=false; if(assignmentError){msg.textContent='حساب ساخته شد، اما تخصیص شعبه کامل نشد: '+assignmentError.message;await loadUsers();return} msg.style.color='var(--good)';msg.textContent='کاربر ایجاد و ایمیل او در فرایند ساخت مدیریتی تأیید شد؛ دسترسی شعبه نیز ثبت شد.';resetForm();await loadUsers(); } }catch(err){msg.textContent='عملیات کاربر به‌دلیل خطای ارتباط یا سرور کامل نشد: '+(err?.message||String(err));} finally{if(btn)btn.disabled=false} }; await loadUsers(); } async function renderCustomerTransfers(){ $('moduleHeading').textContent='انتقال بین مشتریان'; $('moduleDescription').textContent='انتقال مستقیم بین حساب دو مشتری بدون گردش صندوق/بانک، یا انتقال از مسیر دو حساب خزانه. هر دو روش با سند قطعی و ثبت در دفتر کل انجام می‌شوند.'; $('moduleCards').innerHTML=`

ثبت انتقال مشتری

در روش مستقیم فقط مانده حساب دو مشتری جابه‌جا می‌شود. در روش عبور از خزانه، خروج از صندوق/بانک مبدأ و ورود به صندوق/بانک مقصد نیز در همان سند ثبت خواهد شد. مبلغ معادل به ارز پایه باید مطابق نرخ قطعی انتقال وارد شود.

راهنمای کنترل قبل از ثبت

در حال بررسی حساب مشتریان و صندوق‌ها...

`; const [br,pe,cu,ac]=await Promise.all([ client.from('branches').select('id,name').eq('is_active',true).order('name'), client.from('people').select('id,branch_id,full_name,kind').eq('is_active',true).in('kind',['customer','both']).order('full_name').limit(1000), client.from('currencies').select('id,code,name').eq('is_active',true).order('code'), client.from('ledger_accounts').select('id,branch_id,code,name,account_type,account_purpose,person_id').eq('is_active',true).limit(2000) ]); const fail=br.error||pe.error||cu.error||ac.error; if(fail){$('ctReadiness').textContent='دریافت اطلاعات لازم ناموفق بود: '+fail.message;return} const branches=br.data||[],people=pe.data||[],currencies=cu.data||[],accounts=ac.data||[]; fillSelect($('ctBranch'),branches,x=>x.name); fillSelect($('ctCurrency'),currencies,x=>x.code+' — '+x.name); const customerAccounts=accounts.filter(a=>a.account_type==='customer'&&a.person_id); const treasuryAccounts=accounts.filter(a=>a.account_type==='asset'&&['cash','bank'].includes(a.account_purpose)); const fmtDate=()=>new Date(Date.now()-new Date().getTimezoneOffset()*60000).toISOString().slice(0,19); $('ctDate').value=fmtDate(); const optionLabel=x=>x.full_name; function refreshPeople(){ const branch=$('ctBranch').value,eligible=people.filter(p=>p.branch_id===branch&&customerAccounts.filter(a=>a.branch_id===branch&&a.person_id===p.id).length===1); const from=$('ctFrom').value,to=$('ctTo').value; fillSelect($('ctFrom'),eligible,optionLabel); if(eligible.some(p=>p.id===from))$('ctFrom').value=from; const currentFrom=$('ctFrom').value; fillSelect($('ctTo'),eligible.filter(p=>p.id!==currentFrom),optionLabel); if(eligible.some(p=>p.id===to&&p.id!==currentFrom))$('ctTo').value=to; const treasury=treasuryAccounts.filter(a=>a.branch_id===branch); fillSelect($('ctSourceTreasury'),treasury,x=>x.code+' — '+x.name); fillSelect($('ctDestTreasury'),treasury.filter(a=>a.id!==$('ctSourceTreasury').value),x=>x.code+' — '+x.name); $('ctReadiness').textContent=eligible.length+' مشتری دارای دقیقاً یک حساب مشتری فعال در این شعبه شناسایی شد؛ '+Math.max(0,people.filter(p=>p.branch_id===branch).length-eligible.length)+' شخص برای انتقال آماده نیست یا حساب تکراری/ناقص دارد. '+treasury.length+' حساب صندوق/بانک فعال در دسترس است.'; } function refreshDestination(){ const branch=$('ctBranch').value,eligible=people.filter(p=>p.branch_id===branch&&customerAccounts.filter(a=>a.branch_id===branch&&a.person_id===p.id).length===1); const from=$('ctFrom').value,prior=$('ctTo').value; fillSelect($('ctTo'),eligible.filter(p=>p.id!==from),optionLabel); if(prior&&prior!==from&&eligible.some(p=>p.id===prior))$('ctTo').value=prior; const treasury=treasuryAccounts.filter(a=>a.branch_id===branch); const oldDest=$('ctDestTreasury').value,source=$('ctSourceTreasury').value; fillSelect($('ctDestTreasury'),treasury.filter(a=>a.id!==source),x=>x.code+' — '+x.name); if(oldDest!==source&&treasury.some(a=>a.id===oldDest))$('ctDestTreasury').value=oldDest; } function updateMode(){ const via=$('ctMode').value==='treasury'; $('ctSourceWrap').classList.toggle('hidden',!via); $('ctDestWrap').classList.toggle('hidden',!via); $('ctSourceTreasury').required=via;$('ctDestTreasury').required=via; refreshDestination(); } $('ctBranch').onchange=refreshPeople; $('ctFrom').onchange=refreshDestination; $('ctSourceTreasury').onchange=refreshDestination; $('ctMode').onchange=updateMode; refreshPeople();updateMode(); $('ctForm').onsubmit=async e=>{ e.preventDefault(); const msg=$('ctMessage');msg.textContent='';msg.style.color='var(--bad)'; const branch=$('ctBranch').value,from=$('ctFrom').value,to=$('ctTo').value,currency=$('ctCurrency').value; const amount=Number($('ctAmount').value),base=Number($('ctBaseAmount').value),date=$('ctDate').value; if(!branch||!from||!to||from===to||!currency||!(amount>0)||!(base>0)||!date){msg.textContent='همه اطلاعات لازم باید معتبر و کامل باشند؛ مبدأ و مقصد نیز باید متفاوت باشند.';return} const via=$('ctMode').value==='treasury'; if(via&&(!$('ctSourceTreasury').value||!$('ctDestTreasury').value||$('ctSourceTreasury').value===$('ctDestTreasury').value)){msg.textContent='برای انتقال از مسیر خزانه، دو صندوق/بانک متفاوت انتخاب کنید.';return} const args={p_branch_id:branch,p_from_person_id:from,p_to_person_id:to,p_currency_id:currency,p_amount:amount,p_base_amount:base,p_description:$('ctDescription').value.trim()||null,p_occurred_at:new Date(date).toISOString()}; const btn=e.submitter||$('ctForm').querySelector('button[type="submit"]');if(btn)btn.disabled=true;msg.textContent='در حال ثبت سند و قطعی‌کردن انتقال...'; let result; try{ result=via ?await client.rpc('transfer_customer_via_treasury',{...args,p_source_treasury_account_id:$('ctSourceTreasury').value,p_destination_treasury_account_id:$('ctDestTreasury').value}) :await client.rpc('transfer_customer_to_customer',args); }catch(err){msg.textContent='ارتباط با سرور برقرار نشد: '+(err?.message||String(err));return} finally{if(btn)btn.disabled=false} if(result.error){msg.textContent='انتقال انجام نشد: '+result.error.message;return} msg.style.color='var(--good)';msg.textContent='انتقال با موفقیت ثبت و قطعی شد. شناسه سند: '+result.data; $('ctForm').reset();$('ctDate').value=fmtDate();refreshPeople();updateMode(); await loadDashboard(); }; } function renderReports(){ $('moduleHeading').textContent='گزارش سود و زیان'; $('moduleDescription').textContent='گزارش بر اساس اسناد قطعی‌شده و بازه زمانی انتخابی محاسبه می‌شود؛ پیش‌نویس‌ها در گزارش نیستند.'; $('moduleCards').innerHTML='

'; const now=new Date(),first=new Date(now.getFullYear(),now.getMonth(),1),fmt=d=>new Date(d.getTime()-d.getTimezoneOffset()*60000).toISOString().slice(0,10); $('plFrom').value=fmt(first);$('plTo').value=fmt(now); client.from('branches').select('id,name').eq('is_active',true).then(({data,error})=>{if(error){$('plMessage').textContent=error.message;return}for(const b of data||[]){const o=document.createElement('option');o.value=b.id;o.textContent=b.name;$('plBranch').append(o)}}); $('plRun').addEventListener('click',runProfitLossReport);runProfitLossReport(); } async function runProfitLossReport(){ const msg=$('plMessage'),out=$('plResult'),table=$('plCurrencies');if(!msg||!out||!table)return;msg.textContent='';out.replaceChildren();table.replaceChildren(); const from=$('plFrom').value,to=$('plTo').value,branch=$('plBranch').value; if(!from||!to||from>to){msg.textContent='بازه تاریخ معتبر نیست.';return} const [fy,fm,fd]=from.split('-').map(Number),[ty,tm,td]=to.split('-').map(Number);const start=new Date(fy,fm-1,fd,0,0,0,0).toISOString(),end=new Date(ty,tm-1,td+1,0,0,0,0).toISOString(); const btn=$('plRun');btn.disabled=true;msg.textContent='در حال تهیه گزارش...'; const {data,error}=await client.rpc('profit_loss_summary',{p_branch_id:branch||null,p_from:start,p_to:end});btn.disabled=false; if(error){msg.textContent='تهیه گزارش ناموفق بود: '+error.message;return} msg.textContent='گزارش با موفقیت تهیه شد.'; const base=Number(data?.income_base||0)-Number(data?.expense_base||0),fmt=n=>new Intl.NumberFormat('en-US',{maximumFractionDigits:8}).format(Number(n||0)); const cards=[['درآمد به ارز پایه',data?.income_base],['هزینه به ارز پایه',data?.expense_base],['سود / زیان خالص',base]]; for(const [title,value] of cards){const el=document.createElement('div');el.className='module-card';const h=document.createElement('h3');h.textContent=title;const p=document.createElement('p');p.textContent=fmt(value);el.append(h,p);out.append(el)} const rows=data?.by_currency||[];if(!rows.length){table.textContent='در این بازه، درآمد یا هزینه قطعی ثبت نشده است.';return} const t=document.createElement('table');t.style.width='100%';const head=document.createElement('tr');for(const label of ['شعبه','ارز','درآمد','هزینه']){const th=document.createElement('th');th.textContent=label;head.append(th)}const thead=document.createElement('thead');thead.append(head);t.append(thead);const body=document.createElement('tbody'); for(const row of rows){const tr=document.createElement('tr');const vals=[row.branch_name||'—',row.currency_code,fmt(row.income),fmt(row.expense)];for(const v of vals){const td=document.createElement('td');td.textContent=v;tr.append(td)}body.append(tr)}t.append(body);table.append(t); } async function loadDashboard(){ const host=$('currencyBalances'); try{ const {data,error}=await client.rpc('dashboard_summary'); if(error)throw error; const branches=data?.branches||[]; const balances=branches.flatMap(b=>b.balances||[]); const totals=new Map(); for(const b of balances){const old=totals.get(b.currency_code)||{name:b.currency_name,places:b.decimal_places,ledger:0,assets:0};old.ledger+=Number(b.ledger_net||0);old.assets+=Number(b.asset_net||0);totals.set(b.currency_code,old)} const fmt=(n,p)=>new Intl.NumberFormat('en-US',{minimumFractionDigits:0,maximumFractionDigits:p}).format(n); $('cashStats').textContent=[...totals].map(([code,v])=>code+': '+fmt(v.assets,v.places)).join(' · ')||'۰'; $('peopleStats').textContent=[...totals].map(([code,v])=>code+': '+fmt(v.ledger,v.places)).join(' · ')||'۰'; $('txStats').textContent=String(branches.reduce((s,b)=>s+Number(b.today_posted||0),0)); const {data:pl,error:plError}=await client.rpc('profit_loss_summary'); if(plError)throw plError; const baseCode=data?.base_currency?.code||'ارز پایه'; $('profitStats').textContent=baseCode+': '+fmt(Number(pl?.net_base||0),8); const profitCard=$('profitStats').closest('.card'); const profitNote=profitCard?.querySelector('.muted'); if(profitNote)profitNote.textContent='سود خالص ثبت‌شده از ابتدای ماه؛ درآمد '+fmt(Number(pl?.income_base||0),8)+' و هزینه '+fmt(Number(pl?.expense_base||0),8)+' '+baseCode+'؛ بدون سود تحقق‌نیافتهٔ ارزی'; host.replaceChildren(); if(!branches.length){const p=document.createElement('div');p.className='module-card';p.textContent='برای این حساب شعبهٔ فعالی در دسترس نیست.';host.append(p);return} for(const branch of branches){ const card=document.createElement('div');card.className='module-card'; const h=document.createElement('h3');h.textContent=branch.name;card.append(h); const small=document.createElement('p');small.textContent='اسناد پیش‌نویس امروز: '+Number(branch.today_drafts||0);card.append(small); for(const b of branch.balances||[]){const p=document.createElement('p');p.textContent=b.currency_code+' — دفتر کل: '+fmt(Number(b.ledger_net||0),b.decimal_places)+' | دارایی: '+fmt(Number(b.asset_net||0),b.decimal_places);card.append(p)} if(!(branch.balances||[]).length){const p=document.createElement('p');p.textContent='هنوز سند قطعی ثبت نشده است.';card.append(p)} host.append(card); } }catch(e){host.replaceChildren();const p=document.createElement('div');p.className='module-card';p.textContent='دریافت خلاصه داشبورد ناموفق بود. '+(e.message||'');host.append(p)} } let headerClockTimer=null;function updateHeaderClock(){const now=new Date();const dateFa=new Intl.DateTimeFormat('fa-IR',{dateStyle:'full',calendar:'persian',numberingSystem:'latn'}).format(now);const timeFa=new Intl.DateTimeFormat('fa-IR',{timeStyle:'medium',numberingSystem:'latn',hourCycle:'h23'}).format(now);const dateEn=new Intl.DateTimeFormat('en-GB',{dateStyle:'full',calendar:'gregory',numberingSystem:'latn'}).format(now);const timeEn=new Intl.DateTimeFormat('en-GB',{timeStyle:'medium',hourCycle:'h23'}).format(now);$('today').textContent='شمسی: '+dateFa+'، ساعت: '+timeFa+' | میلادی: '+dateEn+'، Time: '+timeEn;}function showApp(){ $('loginView').classList.add('hidden');$('appView').classList.remove('hidden');$('userInfo').textContent=(profile?.full_name||profile?.email||'کاربر')+' · '+(roles[profile?.role]||'بدون نقش');$('sideRole').textContent=roles[profile?.role]||'';renderNav();showPage('dashboard');updateHeaderClock();if(headerClockTimer)clearInterval(headerClockTimer);headerClockTimer=setInterval(updateHeaderClock,1000);loadDashboard()} async function loadProfile(user){const {data,error}=await client.from('profiles').select('id,email,full_name,role,is_active,preferred_language,theme').eq('id',user.id).single();if(error)throw error;if(!data||!data.is_active)throw new Error('حساب کاربری غیرفعال است یا نقش آن تعریف نشده است.');profile=data;lang=['fa','en','ur'].includes(data.preferred_language)?data.preferred_language:'fa';theme=['light','dark','system'].includes(data.theme)?data.theme:'system';const settings=await client.from('app_settings').select('default_calendar').eq('id',true).maybeSingle();if(!settings.error&&['jalali','gregorian'].includes(settings.data?.default_calendar))calendar=settings.data.default_calendar;$('language').value=lang;localStorage.setItem('hesabyar_lang_'+data.id,lang);localStorage.setItem('hesabyar_theme_'+data.id,theme);document.documentElement.lang=lang;document.documentElement.dir=lang==='en'?'ltr':'rtl';applyTheme();} // Login auditing is handled server-side; clients cannot write directly to audit_logs. $('loginForm').addEventListener('submit',async e=>{e.preventDefault();$('loginError').textContent='';if(!client){$('loginError').textContent='اتصال سرور هنوز تنظیم نشده است. ابتدا SUPABASE_URL و SUPABASE_ANON_KEY را در فایل index.html وارد کنید.';return}const {data,error}=await client.auth.signInWithPassword({email:$('email').value.trim(),password:$('password').value});if(error){$('loginError').textContent='ورود ناموفق بود: '+error.message;return}try{await loadProfile(data.user);showApp()}catch(err){await client.auth.signOut();$('loginError').textContent=err.message||'دریافت نقش کاربر ناموفق بود.'}}); let journalAccounts=[],journalCurrencies=[];function fillSelect(el,items,label){el.replaceChildren();for(const x of items){const o=document.createElement('option');o.value=x.id;o.textContent=label(x);el.append(o)}}async function loadJournalOptions(){const [br,ac,cu]=await Promise.all([client.from('branches').select('id,name').eq('is_active',true).order('name'),client.from('ledger_accounts').select('id,branch_id,code,name,account_type,account_purpose').eq('is_active',true).order('code'),client.from('currencies').select('id,code,name,decimal_places').eq('is_active',true).order('code')]);if(br.error||ac.error||cu.error){$('journalMessage').textContent=(br.error||ac.error||cu.error).message;return}fillSelect($('journalBranch'),br.data||[],x=>x.name);journalAccounts=ac.data||[];journalCurrencies=cu.data||[];fillSelect($('journalCurrency'),journalCurrencies,x=>x.code+' — '+x.name);refreshJournalAccountSelects();$('journalDate').value=new Date(Date.now()-new Date().getTimezoneOffset()*60000).toISOString().slice(0,19);$('journalBranch').onchange=refreshJournalAccountSelects;$('journalType').onchange=refreshJournalAccountSelects}function refreshJournalAccountSelects(){const branch=$('journalBranch').value,accounts=journalAccounts.filter(a=>a.branch_id===branch),treasury=accounts.filter(a=>a.account_type==='asset'&&['cash','bank'].includes(a.account_purpose)),counter=accounts.filter(a=>!(['asset'].includes(a.account_type)&&['cash','bank'].includes(a.account_purpose)));fillSelect($('journalSource'),treasury,x=>x.code+' — '+x.name);fillSelect($('journalTarget'),treasury,x=>x.code+' — '+x.name);fillSelect($('journalCounter'),counter,x=>x.code+' — '+x.name);const isTransfer=$('journalType').value==='transfer';$('journalTargetWrap').classList.toggle('hidden',!isTransfer);$('journalCounterWrap').classList.toggle('hidden',isTransfer);$('journalCounter').required=!isTransfer;$('journalTarget').required=isTransfer}async function loadJournalList(){const {data,error}=await client.from('financial_transactions').select('id,transaction_type,description,occurred_at,status').order('occurred_at',{ascending:false}).limit(30);const body=$('journalRows');body.replaceChildren();if(error){const tr=document.createElement('tr'),td=document.createElement('td');td.colSpan=5;td.textContent=error.message;tr.append(td);body.append(tr);return}for(const x of data||[]){const tr=document.createElement('tr'),vals=[formatAppDate(x.occurred_at,{dateStyle:'short',timeStyle:'short'}),x.transaction_type,x.description||'—',x.status==='posted'?'قطعی':x.status==='draft'?'پیش‌نویس':x.status];for(const v of vals){const td=document.createElement('td');td.textContent=v;tr.append(td)}const td=document.createElement('td');if(x.status==='draft'&&['super_admin','manager','accountant'].includes(profile?.role)){const b=document.createElement('button');b.className='btn';b.type='button';b.textContent='قطعی‌کردن';b.addEventListener('click',async()=>{if(!confirm('سند پس از قطعی‌شدن قابل ویرایش یا حذف نیست. اصلاح آن با سند معکوس انجام می‌شود. ادامه می‌دهید؟'))return;b.disabled=true;try{const {error}=await client.rpc('post_financial_transaction',{p_transaction_id:x.id});if(error){alert('قطعی‌کردن ناموفق بود: '+error.message);return}await loadJournalList();await loadDashboard()}catch(err){alert('ارتباط با سرور برقرار نشد: '+(err?.message||String(err)))}finally{b.disabled=false}});td.append(b)}else td.textContent='—';tr.append(td);body.append(tr)}}$('journalRefresh').addEventListener('click',loadJournalList);$('journalForm').addEventListener('submit',async e=>{e.preventDefault();const msg=$('journalMessage');msg.textContent='';const branch=$('journalBranch').value,kind=$('journalType').value,source=$('journalSource').value,target=$('journalTarget').value,counter=$('journalCounter').value,currency=$('journalCurrency').value,amount=Number($('journalAmount').value),base=Number($('journalBase').value),rate=Number($('journalRate').value),rawDate=$('journalDate').value,dateValue=new Date(rawDate);if(!rawDate||Number.isNaN(dateValue.getTime())){msg.textContent='تاریخ و زمان سند معتبر نیست.';return}if(!branch||!source||!currency||!(amount>0&&base>0&&rate>0)){msg.textContent='لطفاً شعبه، حساب، ارز و مبالغ معتبر را انتخاب کنید.';return}if(kind==='transfer'&&(!target||target===source)){msg.textContent='حساب مقصد باید با حساب مبدأ متفاوت باشد.';return}if(kind!=='transfer'&&!counter){msg.textContent='برای دریافت یا پرداخت، حساب مقابل را انتخاب کنید.';return}const lines=[];const line=(account,debit,credit)=>({ledger_account_id:account,currency_id:currency,person_id:null,description:$('journalDescription').value.trim(),debit,credit,base_debit:debit?base:0,base_credit:credit?base:0,exchange_rate:rate});if(kind==='receipt'){lines.push(line(source,amount,0),line(counter,0,amount))}else if(kind==='payment'){lines.push(line(counter,amount,0),line(source,0,amount))}else{lines.push(line(target,amount,0),line(source,0,amount))}const btn=e.submitter||$('journalForm').querySelector('button[type="submit"]');if(btn)btn.disabled=true;let result;try{result=await client.rpc('create_financial_transaction',{p_branch_id:branch,p_transaction_type:kind,p_description:$('journalDescription').value.trim(),p_occurred_at:dateValue.toISOString(),p_lines:lines})}catch(err){msg.textContent='ارتباط با سرور برقرار نشد: '+(err?.message||String(err));return}finally{if(btn)btn.disabled=false}const {data,error}=result||{};if(error){msg.textContent='ثبت سند ناموفق بود: '+error.message;return}msg.style.color='var(--good)';msg.textContent='سند پیش‌نویس ساخته شد. برای قطعی شدن، باید از مسیر تأیید مدیر/حسابدار ثبت شود. شناسه: '+data;$('journalForm').reset();await loadJournalList()});let fxBranches=[],fxAccounts=[],fxCurrencies=[],fxInventoryMappings=[],fxBaseCurrency=null;let fxSuggestedRate=null;async function loadSuggestedFxRate(){fxSuggestedRate=null;const kind=$('fxType').value,conv=kind==='conversion',branch=$('fxBranch').value,currency=$('fxCurrency').value,hint=$('fxRateHint');if(conv){hint.textContent='برای تبدیل مستقیم، معادل ارز پایه را بر اساس ارزش دفتری و سیاست نرخ‌گذاری خود وارد کنید؛ نرخ خرید/فروش یک‌طرفه به‌تنهایی کافی نیست.';return}if(!branch||!currency){hint.textContent='شعبه و ارز را انتخاب کنید.';return}const rateType=kind==='currency_buy'?'cash_buy':'cash_sell';let rateResult;try{rateResult=await client.from('exchange_rates').select('rate,rate_name,effective_at').eq('branch_id',branch).eq('currency_id',currency).eq('rate_type',rateType).lte('effective_at',new Date().toISOString()).order('effective_at',{ascending:false}).limit(1).maybeSingle()}catch(err){hint.textContent='ارتباط با سرور برای دریافت نرخ برقرار نشد: '+(err?.message||String(err));return}const {data,error}=rateResult;if(error){hint.textContent='دریافت نرخ پیشنهادی ممکن نشد: '+error.message;return}if(!data){hint.textContent='برای این ارز و نوع معامله نرخ ثبت‌شده‌ای وجود ندارد؛ معادل پایه را دستی وارد کنید.';return}fxSuggestedRate=Number(data.rate);hint.textContent='نرخ پیشنهادی '+(kind==='currency_buy'?'خرید نقدی':'فروش نقدی')+': '+new Intl.NumberFormat('en-US',{maximumFractionDigits:8}).format(fxSuggestedRate)+' '+(fxBaseCurrency?.code||'ارز پایه')+' برای هر واحد؛ '+(data.rate_name||'بدون نام')+'، '+formatAppDate(data.effective_at,{dateStyle:'short',timeStyle:'short'});calcFxBaseFromRate()}function calcFxBaseFromRate(){if($('fxType').value==='conversion'||!(fxSuggestedRate>0))return;const amount=Number($('fxAmount').value);if(amount>0)$('fxBaseAmount').value=String(amount*fxSuggestedRate)}$('fxType').addEventListener('change',()=>{updateFxMode();loadSuggestedFxRate()});$('fxBranch').addEventListener('change',()=>{refreshFxAccounts();loadSuggestedFxRate()});$('fxCurrency').addEventListener('change',()=>{refreshFxAccounts();loadSuggestedFxRate()});$('fxSourceCurrency').addEventListener('change',refreshFxAccounts);$('fxDestCurrency').addEventListener('change',refreshFxAccounts);$('fxAmount').addEventListener('input',calcFxBaseFromRate);async function loadFxOptions(){const [b,a,c,s,m]=await Promise.all([client.from('branches').select('id,name').eq('is_active',true).order('name'),client.from('ledger_accounts').select('id,branch_id,code,name,account_type,account_purpose').eq('is_active',true).order('code'),client.from('currencies').select('id,code,name_fa,decimal_places').eq('is_active',true).order('code'),client.from('app_settings').select('base_currency_id').eq('id',true).maybeSingle(),client.from('currency_inventory_account_mappings').select('branch_id,currency_id,inventory_ledger_account_id')]);const msg=$('fxMessage');if(b.error||a.error||c.error||s.error||m.error){msg.textContent=(b.error||a.error||c.error||s.error||m.error).message;return}fxBranches=b.data||[];fxAccounts=a.data||[];fxInventoryMappings=m.data||[];fxCurrencies=c.data||[];fxBaseCurrency=fxCurrencies.find(x=>x.id===s.data?.base_currency_id)||null;fillSelect($('fxBranch'),fxBranches,x=>x.name);const foreignCurrencies=fxCurrencies.filter(x=>x.id!==fxBaseCurrency?.id);fillSelect($('fxCurrency'),foreignCurrencies,x=>x.code+' — '+x.name_fa);fillSelect($('fxSourceCurrency'),foreignCurrencies,x=>x.code+' — '+x.name_fa);fillSelect($('fxDestCurrency'),foreignCurrencies,x=>x.code+' — '+x.name_fa);refreshFxAccounts();const now=new Date(),jp=jalaliDateParts(now);$('fxDateJalali').value=String(jp.year).padStart(4,'0')+'/'+String(jp.month).padStart(2,'0')+'/'+String(jp.day).padStart(2,'0')+' '+String(now.getHours()).padStart(2,'0')+':'+String(now.getMinutes()).padStart(2,'0');syncJalaliFxDate();if(!fxBaseCurrency)msg.textContent='ارز پایه در تنظیمات تعیین نشده است؛ ابتدا ارز پایه را مشخص کنید.';await loadSuggestedFxRate()}function fxInventoryAccount(currencyId){const branch=$('fxBranch').value,m=fxInventoryMappings.find(x=>x.branch_id===branch&&x.currency_id===currencyId);return m?fxAccounts.find(x=>x.id===m.inventory_ledger_account_id):null}function refreshFxAccounts(){const branch=$('fxBranch').value,treasury=fxAccounts.filter(x=>x.branch_id===branch&&x.account_type==='asset'&&['cash','bank'].includes(x.account_purpose));fillSelect($('fxBaseAccount'),treasury,x=>x.code+' — '+x.name);const foreign=fxInventoryAccount($('fxCurrency').value),source=fxInventoryAccount($('fxSourceCurrency').value),dest=fxInventoryAccount($('fxDestCurrency').value);fillSelect($('fxForeignAccount'),foreign?[foreign]:[],x=>x.code+' — '+x.name);fillSelect($('fxSourceAccount'),source?[source]:[],x=>x.code+' — '+x.name);fillSelect($('fxDestAccount'),dest?[dest]:[],x=>x.code+' — '+x.name)}function updateFxMode(){const conv=$('fxType').value==='conversion';for(const id of ['fxForeignAccountWrap','fxBaseAccountWrap','fxCurrencyWrap'])$(id).classList.toggle('hidden',conv);$('fxAmountWrap').classList.remove('hidden');$('fxAmountWrap').querySelector('label')?.setAttribute('aria-label',conv?'مقدار ارز مبدأ':'مقدار ارز مورد معامله');$('fxAmountWrap').firstChild.textContent=conv?'مقدار ارز مبدأ':'مقدار ارز مورد معامله';for(const id of ['fxSourceAccountWrap','fxDestAccountWrap','fxSourceCurrencyWrap','fxDestCurrencyWrap','fxDestAmountWrap'])$(id).classList.toggle('hidden',!conv);$('fxForeignAccount').required=!conv;$('fxBaseAccount').required=!conv;$('fxSourceAccount').required=conv;$('fxDestAccount').required=conv;$('fxCurrency').required=!conv;$('fxSourceCurrency').required=conv;$('fxDestCurrency').required=conv;$('fxAmount').required=!conv;$('fxDestAmount').required=conv} $('fxForm').addEventListener('submit',async e=>{e.preventDefault();const msg=$('fxMessage');msg.textContent='';if(!fxBaseCurrency){msg.textContent='ارز پایه تنظیم نشده است.';return}const kind=$('fxType').value,branch=$('fxBranch').value,base=Number($('fxBaseAmount').value),desc=$('fxDescription').value.trim(),rawDate=$('fxDate').value,dateValue=new Date(rawDate);if(!rawDate||Number.isNaN(dateValue.getTime())){msg.textContent='تاریخ و زمان معامله معتبر نیست.';return}const date=dateValue.toISOString();if(!branch||!(base>0)){msg.textContent='شعبه و معادل مثبت ارز پایه الزامی است.';return}const mk=(account,currency,amount,isDebit,rate)=>({ledger_account_id:account,currency_id:currency,person_id:null,description:desc,debit:isDebit?amount:0,credit:isDebit?0:amount,base_debit:isDebit?base:0,base_credit:isDebit?0:base,exchange_rate:rate});let lines=[];if(kind==='currency_buy'||kind==='currency_sell'){const amount=Number($('fxAmount').value),currency=$('fxCurrency').value,foreign=fxInventoryAccount(currency)?.id,baseAccount=$('fxBaseAccount').value;if(!(amount>0)||!currency||!foreign||!baseAccount){msg.textContent='حساب موجودی این ارز تعریف نشده است. مدیر شعبه باید ابتدا حساب موجودی ارز را ایجاد کند.';return}const rate=base/amount;if(kind==='currency_buy')lines=[mk(foreign,currency,amount,true,rate),mk(baseAccount,fxBaseCurrency.id,base,false,1)];else lines=[mk(baseAccount,fxBaseCurrency.id,base,true,1),mk(foreign,currency,amount,false,rate)]}else{const source=$('fxSourceCurrency').value,dest=$('fxDestCurrency').value,sourceAmount=Number($('fxAmount').value),destAmount=Number($('fxDestAmount').value),sourceAccount=fxInventoryAccount(source)?.id,destAccount=fxInventoryAccount(dest)?.id;if(!source||!dest||source===dest||!(sourceAmount>0&&destAmount>0)||!sourceAccount||!destAccount){msg.textContent='ارزهای مبدأ و مقصد باید متفاوت باشند و حساب موجودی هر دو ارز توسط مدیر تعریف شده باشد؛ همه مبالغ نیز باید مثبت باشند.';return}lines=[mk(destAccount,dest,destAmount,true,base/destAmount),mk(sourceAccount,source,sourceAmount,false,base/sourceAmount)]}const btn=e.submitter||$('fxForm').querySelector('button');if(btn)btn.disabled=true;let result;try{result=await client.rpc('create_financial_transaction',{p_branch_id:branch,p_transaction_type:kind,p_description:desc,p_occurred_at:date,p_lines:lines})}catch(err){msg.textContent='ارتباط با سرور برقرار نشد: '+(err?.message||String(err));return}finally{if(btn)btn.disabled=false}const {data,error}=result||{};if(error){msg.textContent='ثبت معامله ناموفق بود: '+error.message;return}msg.style.color='var(--good)';msg.textContent='پیش‌نویس معامله ارزی ساخته شد. برای قطعی‌شدن به تأیید مدیر یا حسابدار نیاز دارد. شناسه: '+data;$('fxForm').reset();$('fxDate').value=new Date(Date.now()-new Date().getTimezoneOffset()*60000).toISOString().slice(0,19)});async function loadRateOptions(){const [b,c,p,settings]=await Promise.all([client.from('branches').select('id,name').eq('is_active',true),client.from('currencies').select('id,code,name_fa').eq('is_active',true).order('code'),client.from('people').select('id,full_name,code,branch_id').eq('is_active',true).order('full_name'),client.from('app_settings').select('base_currency_id').eq('id',true).maybeSingle()]);if(b.error||c.error||p.error||settings.error){$('rateMessage').textContent=(b.error||c.error||p.error||settings.error).message;return}const baseId=settings.data?.base_currency_id,rateCurrencies=(c.data||[]).filter(x=>x.id!==baseId);fillSelect($('rateBranch'),b.data||[],x=>x.name);fillSelect($('rateCurrency'),rateCurrencies,x=>x.code+' — '+x.name_fa);function refreshRatePeople(){const branch=$('rateBranch').value;fillSelect($('ratePerson'),(p.data||[]).filter(x=>x.branch_id===branch),x=>x.full_name+' ('+x.code+')')} $('rateBranch').onchange=refreshRatePeople;refreshRatePeople();$('rateDate').value=new Date(Date.now()-new Date().getTimezoneOffset()*60000).toISOString().slice(0,19)}async function loadRateList(){const {data,error}=await client.from('exchange_rates').select('id,rate_type,rate,rate_name,effective_at,branches(name),currencies(code)').order('effective_at',{ascending:false}).limit(100);const body=$('rateRows');body.replaceChildren();if(error){const tr=document.createElement('tr'),td=document.createElement('td');td.colSpan=6;td.textContent=error.message;tr.append(td);body.append(tr);return}for(const x of data||[]){const tr=document.createElement('tr');const vals=[formatAppDate(x.effective_at,{dateStyle:'short',timeStyle:'short'}),x.branches?.name||'—',x.currencies?.code||'—',({cash_buy:'خرید نقدی',cash_sell:'فروش نقدی',remittance_buy:'خرید حواله',remittance_sell:'فروش حواله',special_customer:'ویژه مشتری',negotiated:'توافقی'})[x.rate_type]||x.rate_type,x.rate_name||'—',new Intl.NumberFormat('en-US',{maximumFractionDigits:8}).format(Number(x.rate))];for(const v of vals){const td=document.createElement('td');td.textContent=v;tr.append(td)}body.append(tr)}}$('rateType').addEventListener('change',()=> $('ratePersonWrap').classList.toggle('hidden',$('rateType').value!=='special_customer'));$('rateRefresh').addEventListener('click',loadRateList);$('rateForm').addEventListener('submit',async e=>{e.preventDefault();const msg=$('rateMessage');msg.textContent='';const kind=$('rateType').value,person=$('ratePerson').value,rawDate=$('rateDate').value,dateValue=new Date(rawDate);if(!rawDate||Number.isNaN(dateValue.getTime())){msg.textContent='تاریخ مؤثر نرخ معتبر نیست.';return}if(kind==='special_customer'&&!person){msg.textContent='برای نرخ ویژه، مشتری را انتخاب کنید.';return}const args={p_branch_id:$('rateBranch').value,p_currency_id:$('rateCurrency').value,p_rate_type:kind,p_rate:Number($('rateValue').value),p_effective_at:dateValue.toISOString(),p_note:$('rateNote').value.trim()||null,p_rate_name:$('rateName').value.trim()||null,p_person_id:kind==='special_customer'?person:null};if(!(args.p_rate>0)||!args.p_branch_id||!args.p_currency_id){msg.textContent='شعبه، ارز و نرخ معتبر الزامی است.';return}const btn=e.submitter||$('rateForm').querySelector('button[type="submit"]');if(btn)btn.disabled=true;let result;try{result=await client.rpc('create_exchange_rate',args)}catch(err){msg.textContent='ارتباط با سرور برقرار نشد: '+(err?.message||String(err));return}finally{if(btn)btn.disabled=false}const {error}=result||{};if(error){msg.textContent='ذخیره نرخ ناموفق بود: '+error.message;return}msg.style.color='var(--good)';msg.textContent='نرخ جدید ذخیره شد.';await loadRateList()});async function loadOpeningOptions(){const branch=$('accountBranch').value;if(!branch)return;const [a,c]=await Promise.all([client.from('ledger_accounts').select('id,code,name,account_type,account_purpose').eq('branch_id',branch).eq('is_active',true),client.from('currencies').select('id,code,name').eq('is_active',true).order('code')]);if(a.error||c.error){$('openingMessage').textContent=(a.error||c.error).message;return}const treasury=(a.data||[]).filter(x=>x.account_type==='asset'&&['cash','bank'].includes(x.account_purpose)),equity=(a.data||[]).filter(x=>x.account_type==='equity');fillSelect($('openingAccount'),treasury,x=>x.code+' — '+x.name);fillSelect($('openingEquity'),equity,x=>x.code+' — '+x.name);fillSelect($('openingCurrency'),c.data||[],x=>x.code+' — '+x.name);$('openingDate').value=new Date(Date.now()-new Date().getTimezoneOffset()*60000).toISOString().slice(0,19)}async function loadAccountBalances(){const {data,error}=await client.rpc('cash_bank_balances');const body=$('accountBalanceRows');body.replaceChildren();if(error){const tr=document.createElement('tr'),td=document.createElement('td');td.colSpan=3;td.textContent=error.message;tr.append(td);body.append(tr);return}for(const x of data||[]){const tr=document.createElement('tr');for(const v of [x.account_code+' — '+x.account_name,x.currency_code,new Intl.NumberFormat('en-US',{maximumFractionDigits:x.decimal_places??8}).format(Number(x.balance))]){const td=document.createElement('td');td.textContent=v;tr.append(td)}body.append(tr)}if(!(data||[]).length){const tr=document.createElement('tr'),td=document.createElement('td');td.colSpan=3;td.textContent='هنوز ماندهٔ ثبت‌شده‌ای در اسناد قطعی وجود ندارد.';tr.append(td);body.append(tr)}}let accountCache=[];async function loadAccountOptions(){const branch=$('accountBranch'),person=$('accountPerson');if(branch.dataset.loaded!=='1'){const {data,error}=await client.from('branches').select('id,name').eq('is_active',true).order('name');if(error){$('accountMessage').textContent=error.message;return}branch.replaceChildren();for(const x of data||[]){const o=document.createElement('option');o.value=x.id;o.textContent=x.name;branch.append(o)}branch.dataset.loaded='1';branch.onchange=async()=>{await loadOpeningOptions();await loadAccounts()}}if(person.dataset.loaded!=='1'){const {data,error}=await client.from('people').select('id,full_name').eq('is_active',true).order('full_name').limit(500);if(error){$('accountMessage').textContent=error.message;return}for(const x of data||[]){const o=document.createElement('option');o.value=x.id;o.textContent=x.full_name;person.append(o)}person.dataset.loaded='1'}}function renderAccounts(){const q=$('accountSearch').value.trim().toLocaleLowerCase(),rows=accountCache.filter(a=>[a.name,a.code].some(v=>String(v||'').toLocaleLowerCase().includes(q)));const body=$('accountRows');body.replaceChildren();if(!rows.length){const tr=document.createElement('tr'),td=document.createElement('td');td.colSpan=6;td.textContent='حسابی یافت نشد.';tr.append(td);body.append(tr)}for(const a of rows){const tr=document.createElement('tr'),vals=[a.code,a.name,a.account_purpose==='cash'?'صندوق نقدی':'حساب بانکی',a.branches?.name||'—',a.people?.full_name||'—',a.is_active?'فعال':'غیرفعال'];for(const v of vals){const td=document.createElement('td');td.textContent=v;tr.append(td)}body.append(tr)}$('accountCount').textContent='تعداد: '+rows.length+' از '+accountCache.length}async function loadAccounts(){await loadOpeningOptions();await loadAccountBalances();const {data,error}=await client.from('ledger_accounts').select('id,branch_id,code,name,account_type,account_purpose,person_id,is_active,branches(name),people(full_name)').eq('account_type','asset').not('account_purpose','is',null).order('created_at',{ascending:false}).limit(500);if(error){$('accountRows').replaceChildren();const tr=document.createElement('tr'),td=document.createElement('td');td.colSpan=6;td.textContent='دریافت حساب‌ها ناموفق بود: '+error.message;tr.append(td);$('accountRows').append(tr);return}accountCache=data||[];renderAccounts()}$('accountSearch').addEventListener('input',renderAccounts);$('accountRefresh').addEventListener('click',loadAccounts);$('balancesRefresh').addEventListener('click',loadAccountBalances);$('openingForm').addEventListener('submit',async e=>{e.preventDefault();const msg=$('openingMessage');msg.textContent='';const rawDate=$('openingDate').value,dateValue=new Date(rawDate);if(!rawDate||Number.isNaN(dateValue.getTime())){msg.textContent='تاریخ مانده افتتاحیه معتبر نیست.';return}const payload={p_branch_id:$('accountBranch').value,p_account_id:$('openingAccount').value,p_opening_equity_account_id:$('openingEquity').value,p_currency_id:$('openingCurrency').value,p_side:$('openingSide').value,p_amount:Number($('openingAmount').value),p_base_amount:Number($('openingBase').value),p_exchange_rate:Number($('openingRate').value),p_occurred_at:dateValue.toISOString(),p_description:$('openingDescription').value.trim()};if(!payload.p_account_id||!payload.p_opening_equity_account_id||!payload.p_currency_id||!(payload.p_amount>0&&payload.p_base_amount>0&&payload.p_exchange_rate>0)){msg.textContent='اطلاعات حساب، ارز و مبالغ را بررسی کنید.';return}const btn=e.submitter||$('openingForm').querySelector('button[type="submit"]');if(btn)btn.disabled=true;let result;try{result=await client.rpc('record_opening_balance',payload)}catch(err){msg.textContent='ارتباط با سرور برقرار نشد: '+(err?.message||String(err));return}finally{if(btn)btn.disabled=false}const {data,error}=result||{};if(error){msg.textContent='ثبت مانده اولیه ناموفق بود: '+error.message;return}msg.style.color='var(--good)';msg.textContent='مانده اولیه با موفقیت ثبت شد. شناسه سند: '+data;await loadAccountBalances()});$('accountForm').addEventListener('submit',async e=>{e.preventDefault();const msg=$('accountMessage');msg.textContent='';const payload={p_branch_id:$('accountBranch').value,p_code:$('accountCode').value.trim(),p_name:$('accountName').value.trim(),p_account_type:'asset',p_person_id:$('accountPerson').value||null,p_account_purpose:$('accountPurpose').value};if(!payload.p_branch_id){msg.textContent='ابتدا شعبه فعال ایجاد کنید.';return}const btn=e.submitter||$('accountForm').querySelector('button[type="submit"]');if(btn)btn.disabled=true;let result;try{result=await client.rpc('create_ledger_account',payload)}catch(err){msg.textContent='ارتباط با سرور برقرار نشد: '+(err?.message||String(err));return}finally{if(btn)btn.disabled=false}const {error}=result||{};if(error){msg.textContent='ایجاد حساب ناموفق بود: '+error.message;return}msg.style.color='var(--good)';msg.textContent='حساب با موفقیت ایجاد شد.';$('accountForm').reset();$('accountBranch').dataset.loaded='1';$('accountPerson').dataset.loaded='1';await loadAccounts()});let peopleCache=[];async function loadBranches(){const sel=$('personBranch');if(sel.dataset.loaded==='1')return;const {data,error}=await client.from('branches').select('id,name').eq('is_active',true).order('name');if(error){$('personMessage').textContent='دریافت شعبه‌ها ناموفق بود: '+error.message;return}sel.replaceChildren();for(const b of data||[]){const o=document.createElement('option');o.value=b.id;o.textContent=b.name;sel.append(o)}sel.dataset.loaded='1'}function renderPeople(){const q=$('peopleSearch').value.trim().toLocaleLowerCase();const rows=peopleCache.filter(p=>[p.full_name,p.code,p.phone,p.email].some(v=>String(v||'').toLocaleLowerCase().includes(q)));const body=$('peopleRows');body.replaceChildren();if(!rows.length){const tr=document.createElement('tr'),td=document.createElement('td');td.colSpan=6;td.textContent='موردی پیدا نشد.';tr.append(td);body.append(tr)}for(const p of rows){const tr=document.createElement('tr');const vals=[p.full_name,p.code||'—',({customer:'مشتری',supplier:'تأمین‌کننده',both:'مشتری و تأمین‌کننده',other:'سایر'})[p.kind]||p.kind,p.phone||'—',p.email||'—'];for(const v of vals){const td=document.createElement('td');td.textContent=v;tr.append(td)}const td=document.createElement('td'),s=document.createElement('span');s.className='status';s.textContent=p.is_active?'فعال':'غیرفعال';td.append(s);tr.append(td);body.append(tr)}$('peopleCount').textContent='تعداد نمایش: '+rows.length+' از '+peopleCache.length}async function loadPeople(){$('peopleRows').innerHTML='در حال دریافت...';const {data,error}=await client.from('people').select('id,branch_id,code,full_name,kind,phone,email,is_active,created_at').order('created_at',{ascending:false}).limit(500);if(error){$('peopleRows').replaceChildren();const tr=document.createElement('tr'),td=document.createElement('td');td.colSpan=6;td.textContent='دریافت فهرست ناموفق بود: '+error.message;tr.append(td);$('peopleRows').append(tr);return}peopleCache=data||[];renderPeople()}$('peopleSearch').addEventListener('input',renderPeople);$('peopleRefresh').addEventListener('click',loadPeople);$('personForm').addEventListener('submit',async e=>{e.preventDefault();const msg=$('personMessage');msg.textContent='';msg.style.color='var(--bad)';const payload={p_branch_id:$('personBranch').value,p_code:$('personCode').value.trim()||null,p_full_name:$('personName').value.trim(),p_kind:$('personKind').value,p_phone:$('personPhone').value.trim()||null,p_email:$('personEmail').value.trim()||null,p_address:$('personAddress').value.trim()||null,p_notes:$('personNotes').value.trim()||null};if(!payload.p_branch_id){msg.textContent='ابتدا شعبه فعال ایجاد کنید.';return}const btn=e.submitter||$('personForm').querySelector('button[type="submit"]');if(btn)btn.disabled=true;let result;try{result=await client.rpc('create_person',payload)}catch(err){msg.textContent='ارتباط با سرور برقرار نشد: '+(err?.message||String(err));return}finally{if(btn)btn.disabled=false}const {error}=result||{};if(error){msg.textContent='ثبت انجام نشد: '+error.message;return}msg.style.color='var(--good)';msg.textContent='شخص با موفقیت ثبت شد.';$('personForm').reset();$('personBranch').dataset.loaded='1';await loadPeople()});$('logoutBtn').addEventListener('click',async()=>{if(client)await client.auth.signOut();profile=null;$('appView').classList.add('hidden');$('loginView').classList.remove('hidden')}); function toggleTheme(){theme=theme==='light'?'dark':'light';applyTheme();if(profile?.id)saveProfilePreferences({theme}).catch(err=>console.warn('Could not sync theme preference:',err.message))}$('themeBtn').onclick=toggleTheme;$('mobileTheme').onclick=toggleTheme; $('language').value=lang;$('language').addEventListener('change',async e=>{lang=e.target.value;localStorage.setItem('hesabyar_lang',lang);if(profile?.id)localStorage.setItem('hesabyar_lang_'+profile.id,lang);document.documentElement.lang=lang;document.documentElement.dir=lang==='en'?'ltr':'rtl';if(profile){showApp();try{await saveProfilePreferences({preferred_language:lang})}catch(err){console.warn('Could not sync language preference:',err.message)}}});document.querySelectorAll('[data-module]').forEach(b=>b.addEventListener('click',()=>showPage(b.dataset.module))); applyTheme();if(SUPABASE_URL.startsWith('https://')&&SUPABASE_ANON_KEY!=='YOUR_SUPABASE_ANON_KEY'&&window.supabase){client=window.supabase.createClient(SUPABASE_URL,SUPABASE_ANON_KEY);client.auth.getSession().then(async({data})=>{if(data.session){try{await loadProfile(data.session.user);showApp()}catch(e){console.warn(e.message)}}})}