`;
}
function wireDealButtons(container){
container.querySelectorAll('[data-load-deal]').forEach(button=>button.addEventListener('click',()=>loadDeal(button.dataset.loadDeal)));
container.querySelectorAll('[data-complete-deal]').forEach(button=>button.addEventListener('click',()=>loadDeal(button.dataset.completeDeal,true)));
container.querySelectorAll('[data-void-deal]').forEach(button=>button.addEventListener('click',async()=>{const entry=state.transactions.find(t=>t.id===button.dataset.voidDeal);if(!confirm('Void “'+entry.name+'”? It will stay in history and be excluded from profit statistics.'))return;button.disabled=true;try{await api('void_transaction',{id:entry.id});await refresh();toast('Record voided. Statistics updated.');}catch(error){toast(error.message);button.disabled=false;}}));
}
function renderLedger(){
const search=$('ledger-search').value.trim().toLowerCase(),kind=$('ledger-kind').value,status=$('ledger-status').value;
const entries=state.transactions.filter(t=>(kind==='all'||t.kind===kind)&&(status==='all'||t.status===status)&&String(t.name).toLowerCase().includes(search)).slice().sort((a,b)=>b.occurredOn.localeCompare(a.occurredOn)||String(b.createdAt||'').localeCompare(String(a.createdAt||'')));
$('ledger-list').innerHTML=entries.length?entries.map(t=>dealRow(t)).join(''):'
No deals match this view.
';wireDealButtons($('ledger-list'));
}
for(const id of ['ledger-search','ledger-kind','ledger-status'])$(id).addEventListener(id.endsWith('search')?'input':'change',renderLedger);
function selectControl(kind,key,label,options,help=''){return `
${help?`
${esc(help)}
`:''}
`;}
function numberControl(kind,key,label,unit='',help=''){return `
${esc(unit)}
${help?`
${esc(help)}
`:''}
`;}
function calculatorHTML(kind){
const n=(key,label,unit='',help='')=>numberControl(kind,key,label,unit,help),s=(key,label,opts,help='')=>selectControl(kind,key,label,opts,help);
const isDebit=kind==='debit';
const inputHTML=isDebit?`
${s('amountMode','Start with',[['cash','OMR amount — calculate INR and USDT'],['usdt','USDT quantity — calculate OMR and INR'],['actual','Actual trade — enter actual amounts']])}
${n('estimatedQuantity','Calculated USDT quantity','USDT')}${n('targetQuantity','USDT quantity to buy / sell','USDT')}${n('actualQuantity','Actual USDT received / sold','USDT')}
Other costs and sale fees
${n('buyFee','Buy fee paid from OMR cash','OMR')}${n('otherCost','Other INR costs','INR')}${n('sellPercent','Sale fee percentage','%')}${n('sellFee','Fixed sale fee','INR')}
`:`
${n('receivedOMR','OMR received from customer','OMR')}
${s('payoutMode','Your promise to the customer',[['rate','Google rate + bonus per OMR'],['total','Exact INR amount to send']])}
${n('googleRate','Google INR/OMR rate','INR / OMR')}${n('bonusPerOMR','Bonus you promise per OMR','INR / OMR')}
${n('payoutINR','INR you will send the customer','INR')}
Enter today’s Google quote yourself. Payout = OMR × (Google rate + bonus). The prefilled rate is an example, not a live quote.
${s('quantityMode','USDT quantity',[['estimated','Calculate from the OMR received'],['actual','Enter the actual USDT received']])}
${n('estimatedQuantity','Calculated USDT quantity','USDT')}${n('actualQuantity','Actual USDT received / sold','USDT')}
Optional other costs / sale fees
Bank, card markup and ATM fees are zero in this section.
${n('otherCost','Other INR costs','INR')}${n('sellPercent','Sale fee percentage','%')}${n('sellFee','Fixed sale fee','INR')}
`;
return ``;
}
function createController(kind){
$(kind+'-calculator').innerHTML=calculatorHTML(kind);
const base=kind==='debit'?DEFAULTS:DIRECT_DEFAULTS;
const c={kind,defaults:base,form:$(kind+'-form'),result:null,id:crypto.randomUUID(),prospectId:null,isExisting:false,saving:false};
c.values=()=>Object.fromEntries(Object.keys(base).map(key=>[key,key==='feesIncluded'?$(kind+'-'+key).checked:$(kind+'-'+key).value]));
c.fill=inputs=>{const values={...base,...inputs};for(const key of Object.keys(base)){const input=$(kind+'-'+key);if(key==='feesIncluded')input.checked=values[key]===true;else input.value=values[key];}};
c.update=()=>{
const values=c.values(),d=kind==='debit';
const show=(key,visible)=>$(kind+'-'+key+'-field').hidden=!visible;
const derived=(key,yes)=>{const input=$(kind+'-'+key);input.readOnly=yes;input.closest('.input-unit')?.classList.toggle('derived',yes);};
if(d){
show('inrRate',values.conversionMode==='rate');show('forexPercent',values.forexMode==='percent');show('forexFee',values.forexMode==='fixed');show('feesIncluded',values.conversionMode==='debit');
if(values.conversionMode==='rate'){$('debit-feesIncluded').checked=false;values.feesIncluded=false;}
show('estimatedQuantity',values.amountMode==='cash');show('targetQuantity',values.amountMode==='usdt');show('actualQuantity',values.amountMode==='actual');
derived('withdrawal',values.amountMode==='usdt');derived('debit',values.conversionMode==='rate');
}else{
show('googleRate',values.payoutMode==='rate');show('bonusPerOMR',values.payoutMode==='rate');$('direct-bonus-presets').hidden=values.payoutMode!=='rate';
show('estimatedQuantity',values.quantityMode==='estimated');show('actualQuantity',values.quantityMode==='actual');derived('payoutINR',values.payoutMode==='rate');
$('direct-confirmations').hidden=$('direct-status').value!=='completed';
}
derived('estimatedQuantity',true);
c.result=calculateTransaction(kind,values);
for(const key of Object.keys(base)){const input=$(kind+'-'+key);input.closest('.field')?.classList.toggle('invalid',!!c.result.errors[key]);input.setAttribute('aria-invalid',c.result.errors[key]?'true':'false');if($(kind+'-'+key+'-error'))$(kind+'-'+key+'-error').textContent=c.result.errors[key]||'';}
$(kind+'-save').disabled=c.saving||!c.result.valid;
$(kind+'-error').hidden=c.result.valid;
const completed=$(kind+'-status').value==='completed';
$(kind+'-save-guidance').textContent=completed?'This actual completed deal will count in profit statistics.':'A saved plan or voided record does not count as earned profit.';
if(!c.result.valid){for(const key of ['profit','quantity','roi','break-even'])$(kind+'-'+key).textContent='—';$(kind+'-result-lines').replaceChildren();$(kind+'-cash-note').textContent='Enter valid values to see the full breakdown.';$(kind+'-profit-label').textContent='CHECK YOUR INPUTS';return;}
const r=c.result;
if(d){if(values.amountMode==='usdt')$('debit-withdrawal').value=r.withdrawal;if(values.conversionMode==='rate')$('debit-debit').value=r.debit;}
else if(values.payoutMode==='rate')$('direct-payoutINR').value=r.payoutINR;
$(kind+'-estimatedQuantity').value=r.quantity;
$(kind+'-profit').textContent=money(Math.abs(r.profit)<0.005?0:r.profit);$(kind+'-quantity').textContent=num(r.quantity,4);$(kind+'-roi').textContent=r.roi===null?'—':num(r.roi,2)+'%';
$(kind+'-profit-box').classList.toggle('loss',r.profit<-.005);$(kind+'-profit-label').textContent=(completed?'DEAL ':'ESTIMATED ')+(r.profit<-.005?'LOSS':Math.abs(r.profit)<.005?'BREAK-EVEN':'PROFIT');
$(kind+'-profit-note').textContent=d?'After bank/card charges and selling costs':'After the customer’s INR payout and entered costs';
const lines=d?[['Card debit',r.debit],['Forex / markup'+(r.input.feesIncluded?' (included)':''),r.forex],['ATM fee'+(r.input.feesIncluded?' (included)':''),Number(r.data.atmFee)],['Other costs',Number(r.data.otherCost)],['Total cost',r.cost,true],['Gross USDT sale',r.gross],['Sale fees',r.saleFees],['Net sale proceeds',r.net,true]]:[['INR promised to customer',r.payoutINR],['Other costs',Number(r.data.otherCost)],['Total payout / cost',r.cost,true],['Gross USDT sale',r.gross],['Sale fees',r.saleFees],['Net sale proceeds',r.net,true]];
$(kind+'-result-lines').innerHTML=lines.map(([label,value,total])=>`
${esc(label)}
${esc(money(value))}
`).join('');
$(kind+'-break-even').textContent='₹'+num(r.breakEven,4)+' / USDT';
const mismatch=r.remainingOMR < -0.0000001;
$(kind+'-cash-note').classList.toggle('warning',mismatch);
$(kind+'-cash-note').textContent=mismatch?'The actual USDT entered costs more OMR than the available cash. Check your quantity or buy price.':r.remainingOMR>0.0000001?num(r.remainingOMR)+' OMR remains, separate from profit.':'All '+num(r.withdrawal)+' OMR is used in this calculation.';
};
c.reset=(inputs,meta={})=>{
c.id=meta.id||crypto.randomUUID();c.isExisting=!!meta.id;c.prospectId=meta.prospectId||null;
c.fill(inputs||state.settings[kind==='debit'?'debitDefaults':'directDefaults']);
$(kind+'-name').value=meta.name||'';$(kind+'-occurredOn').value=meta.occurredOn||state.today;$(kind+'-status').value=meta.status||'planned';$(kind+'-save-status').textContent='';
if(kind==='direct'){$('direct-fundsReceived').checked=false;$('direct-payoutCompleted').checked=false;$('prospect-banner').hidden=!c.prospectId;if(c.prospectId){const p=state.prospects.find(p=>p.id===c.prospectId);$('prospect-banner').textContent='Customer opportunity: '+(p?.name||meta.name)+'. This will enter profit stats only after receipt and payout are completed.';}}
c.update();
};
c.form.addEventListener('input',event=>{if(event.target.name==='amountMode')return;$(kind+'-save-status').textContent='';c.update();});
c.form.addEventListener('change',event=>{if(kind==='debit'&&event.target.name==='amountMode'&&c.result?.valid){if(event.target.value==='usdt')$('debit-targetQuantity').value=c.result.quantity;if(event.target.value==='actual')$('debit-actualQuantity').value=c.result.quantity;}c.update();});
c.form.addEventListener('submit',async event=>{
event.preventDefault();c.update();const status=$(kind+'-save-status');
if(!c.result.valid){status.textContent='Check the highlighted inputs.';$(kind+'-'+Object.keys(c.result.errors)[0])?.focus();return;}
const name=$(kind+'-name').value.trim(),occurredOn=$(kind+'-occurredOn').value,recordStatus=$(kind+'-status').value;
if(!name){status.textContent='Give the deal a name first.';$(kind+'-name').focus();return;}
if(!occurredOn){status.textContent='Choose a deal date.';return;}
if(recordStatus==='completed'&&occurredOn>state.today){status.textContent='Future deals should be saved as plans, then completed on the actual day.';return;}
const flags=kind==='direct'?{fundsReceived:$('direct-fundsReceived').checked,payoutCompleted:$('direct-payoutCompleted').checked}:{};
if(kind==='direct'&&recordStatus==='completed'&&(!flags.fundsReceived||!flags.payoutCompleted)){status.textContent='Confirm OMR received and INR payout completed before recording real profit.';return;}
const inputs={...c.result.input};
for(const key of Object.keys(inputs))if(!Object.hasOwn(base,key))delete inputs[key];
for(const [key,fallback]of Object.entries(base))if(typeof fallback==='number'&&(inputs[key]===''||!Number.isFinite(Number(inputs[key]))||Number(inputs[key])<0))inputs[key]=fallback;
const transaction={id:c.id,kind,status:recordStatus,name,occurredOn,inputs,...(c.prospectId?{prospectId:c.prospectId}:{})};
c.saving=true;c.update();status.textContent='Saving to your server…';
try{
const previous=state.transactions.find(entry=>entry.id===c.id);
const firstProspectCompletion=c.prospectId&&recordStatus==='completed'&&(!previous||previous.status==='planned')&&state.prospects.find(p=>p.id===c.prospectId)?.stage!=='completed';
const action=firstProspectCompletion?'complete_prospect':c.isExisting?'update_transaction':'save_transaction';
const payload=action==='complete_prospect'?{prospectId:c.prospectId,transaction,...flags}:{transaction,...flags};
const saved=await api(action,payload);rememberTransaction(c,saved.transaction);await refresh();status.textContent=recordStatus==='completed'?'Completed deal saved. Your statistics are updated.':'Record saved. Plans stay separate from earned profit.';toast(status.textContent);
}catch(error){const duplicate=['already_saved','already_completed','already_linked'].includes(error.code)&&error.transaction&&(error.transaction.id===c.id||(c.prospectId&&error.transaction.prospectId===c.prospectId));if(duplicate){rememberTransaction(c,error.transaction);try{await refresh();status.textContent='This deal is already saved and has not been counted twice. Review your values and save again to update it.';}catch{status.textContent='The server already has this deal. Reconnect and save again to update it.';}}else status.textContent=error.message;}
finally{c.saving=false;c.update();}
});
$(kind+'-new').addEventListener('click',()=>c.reset());
controllers[kind]=c;return c;
}
function rememberTransaction(controller,entry){if(!entry)return;controller.id=entry.id;controller.isExisting=true;const position=state.transactions.findIndex(t=>t.id===entry.id);if(position<0)state.transactions.push(entry);else state.transactions[position]=entry;}
function loadDeal(id,complete=false){const entry=state.transactions.find(t=>t.id===id);if(!entry)return;controllers[entry.kind].reset(entry.inputs,{...entry,status:complete?'completed':entry.status});location.hash=entry.kind;toast(complete?'Review the actual amounts and confirm completion, then save.':'Saved deal loaded. Changes require Save deal.');}
function renderCustomers(){
const search=$('customer-search').value.trim().toLowerCase();
const records=state.prospects.filter(p=>(p.name+' '+(p.contact||'')).toLowerCase().includes(search));
$('customer-list').innerHTML=records.length?records.map(p=>{
const r=calculateTransaction('direct',{...state.settings.directDefaults,receivedOMR:p.expectedAmountOMR,quantityMode:'estimated'});
const complete=p.stage==='completed'||state.transactions.some(t=>t.prospectId===p.id&&t.status==='completed');
return `
${esc(p.name)}
${esc(p.contact||'No contact added')} · Salary day ${esc(p.payday||'—')}