1// Ayarlar â "Fatura & ödeme" ekranı â her satır GERÃEK bir uçtan. 2// 3// NEDEN VAR: bu panel prototipten kalma sabit HTML'di ve canlıda herkese AYNI 4// uydurma satırları gösteriyordu â "Morfoz Plus · $9.95/ay", "Visa â¢â¢â¢â¢ 4242", 5// "Aigap Yazılım A.Å. · VKN: 0123456789", %64 dolu kullanım çubuÄu, açık 6// konumda bir "otomatik yenileme" anahtarı. Hiçbiri kullanıcının verisi 7// deÄildi; 4242 zaten Stripe'ın test kartı numarası. 8// 9// Bu, sızıntıdan çok GÃVEN kusuruydu ama sonuçları gerçekti: müÅteri hiç kart 10// tanımlamadıÄı hâlde hesabında kayıtlı kart gördü ve haklı olarak "bizim 11// kartlarımız birbirine mi karıÅıyor" diye sordu. Fatura adresi satırı ise her 12// müÅteriye BAÅKASININ Åirket bilgisini gösteriyordu. 13// 14// KURAL: karÅılıÄı olan uç yoksa satır ÃİZİLMEZ. BoÅ bırakmak uydurmaktan 15// iyidir â bu ekranın tamamı zaten o hatanın bedelini ödedi. Bu yüzden burada 16// artık depolama satırı ve "otomatik yenileme" anahtarı YOK: ikisinin de 17// arkasında hiçbir Åey yoktu (otomatik yenileme için depoda tahsilat iÅi bile 18// bulunmuyor, anahtar açık görünmek dıÅında hiçbir Åey yapmıyordu). 19 20import { api, escapeHtml, fmtDate, toast } from './api-client.js'; 21import { t, currentLang } from './i18n.js'; 22import { iptalKutusunuCiz } from './uyelik-iptal.js'; 23 24function tt(key, fallback) { 25 const v = t(key); 26 return (!v || v === key) ? fallback : v; 27} 28 29/** Yalnız gün â yenileme tarihini saat/dakikayla göstermek yanıltıcı olurdu. */ 30function gun(s) { 31 if (!s) return ''; 32 try { 33 const d = new Date(String(s).replace(' ', 'T') + 'Z'); 34 if (isNaN(d)) return String(s).slice(0, 10); 35 return d.toLocaleDateString(undefined, 36 { day: '2-digit', month: 'short', year: 'numeric' }); 37 } catch { return String(s).slice(0, 10); } 38} 39 40/** Sunucu para birimini kod olarak veriyor (USD/TRY); simgeye çevir. */ 41function para(kurus, birim) { 42 if (kurus == null) return ''; 43 const simge = { USD: '$', TRY: 'âº', EUR: 'â¬', GBP: '£' }[String(birim || '').toUpperCase()]; 44 const tutar = (kurus / 100).toFixed(2); 45 return simge ? `${simge}${tutar}` : `${tutar} ${escapeHtml(birim || '')}`.trim(); 46} 47 48function satir(ad, aciklama, kontrolHtml) { 49 return ` 50 <div class="setting-row"> 51 <div class="setting-info"> 52 <div class="setting-name">${ad}</div> 53 <div class="setting-desc">${aciklama}</div> 54 </div> 55 ${kontrolHtml ? `<div class="setting-control">${kontrolHtml}</div>` : ''} 56 </div>`; 57} 58 59/**
60 * Ekranı çiz. Her uç AYRI yakalanır (allSettled): kayıtlı kart servisi 61 * yanıt vermediÄinde plan ve fatura geçmiÅi de kaybolmasın â eskiden tek bir 62 * hata tüm ekranı boÅ bırakırdı. 63 */ 64export async function faturaOdemeCiz() { 65 const host = document.getElementById('faturaOdemeHost'); 66 if (!host) return; 67 if (!host.dataset.mzYuklendi) { 68 host.innerHTML = `<div style="padding:26px;text-align:center;color:var(--text-tertiary); 69 font-size:12.5px;">${escapeHtml(tt('panel.fo_loading', 'Yükleniyorâ¦'))}</div>`; 70 } 71 72 const sonuc = await Promise.allSettled([ 73 api.get('/api/memberships/active'), 74 api.get('/api/memberships/credits'), 75 api.get('/api/paytr/cards'), 76 api.get('/api/billing/addresses'), 77 api.get('/api/memberships/orders'), 78 katalogAl(), 79 ]); 80 const d = i => (sonuc[i].status === 'fulfilled' ? sonuc[i].value : null); 81 const active = d(0) || {}; 82 const kredi = d(1); 83 const kartlar = (d(2) || {}).cards || []; 84 const adresler = (d(3) || {}).addresses || []; 85 const siparisler = d(4) || []; 86 const katalog = d(5) || {}; 87 88 const slug = String(active.tier || 'free').toLowerCase(); 89 const ucretli = slug && slug !== 'free'; 90 const plan = (katalog.tiers || []).find(x => String(x.tier).toLowerCase() === slug); 91 const planAdi = (plan && plan.name) || (slug.charAt(0).toUpperCase() + slug.slice(1)); 92 93 host.dataset.mzYuklendi = '1'; 94 host.innerHTML = [ 95 planKarti(active, plan, planAdi, ucretli), 96 kullanimBolumu(kredi, plan), 97 kartBolumu(kartlar), 98 adresBolumu(adresler), 99 faturaBolumu(siparisler), 100 ].join(''); 101 102 bagla(host, kartlar); 103 104 // İPTAL KUTUSU tek kaynaktan: "ÃyeliÄim" ekranıyla aynı modül. Buraya ikinci 105 // bir iptal düÄmesi yazmak, iki farklı davranıÅın canlıda ayrıÅması demekti â 106 // bu panelde duran eski düÄme zaten hiçbir yere BAÄLI DEÄİLDİ, tıklayan 107 // kullanıcı hiçbir Åey olmadıÄını görüyordu. 108 try { 109 await iptalKutusunuCiz(host, active, () => faturaOdemeCiz().catch(() => {})); 110 } catch (e) { 111 console.error('[fatura-odeme] iptal kutusu çizilemedi', e); 112 } 113} 114 115function planKarti(active, plan, planAdi, ucretli) { 116 const fiyat = (plan && plan.price && plan.price.display_text) 117 || para(active.price_cents, active.currency); 118 const bitis = active.period_end ? gun(active.period_end) : ''; 119 // 'cancelling' = iptal onaylanmıÅ, eriÅim dönem sonuna kadar sürüyor. 120 // Bu durumda "sonraki yenileme" YAZMAK yalan olur; dönem SONU yazılır. 121 const iptalEdiliyor = String(active.status || '') === 'cancelling'; 122 let alt = ''; 123 if (!ucretli) { 124 alt = escapeHtml(tt('panel.fo_free_desc', 'Ãcretsiz plandasınız.')); 125 } else { 126 const parcalar = []; 127 if (fiyat) parcalar.push(escapeHtml(fiyat)); 128 if (bitis) { 129 parcalar.push(escapeHtml( 130 (iptalEdiliyor ? tt('panel.fo_period_end', 'EriÅim bitiÅi: {d}') 131 : tt('panel.fo_next_renew', 'Sonraki yenileme: {d}')) 132 .replace('{d}', bitis))); 133 } 134 alt = parcalar.join(' · '); 135 } 136 return ` 137 <div class="plan-card"> 138 <div> 139 <div class="current-plan-label">${escapeHtml(tt('panel.mb_your_plan', 'Mevcut planınız'))}</div> 140 <div class="current-plan-name">Morfoz <em>${escapeHtml(planAdi)}</em></div> 141 <div class="current-plan-renew">${alt}</div> 142 </div> 143 <button class="upgrade-btn" data-fo-plans>${escapeHtml( 144 ucretli ? tt('panel.fo_change_plan', 'Planı deÄiÅtir â') 145 : tt('panel.fo_see_plans', 'Planları gör â'))}</button> 146 </div>`; 147} 148 149function kullanimBolumu(kredi, plan) { 150 if (!kredi) return ''; 151 const limits = (plan && plan.limits) || {}; 152 const satirlar = []; 153 154 // Mesaj kredisi: tavanı da kullanımı da sunucu veriyor. "Sınırsız" yalnız 155 // sunucu öyle DERSE yazılır (unlimited) â tavansızlıÄı istemcide tahmin 156 // etmek, tavana takılan kullanıcıya "sınırsız" göstermenin yoluydu. 157 if (kredi.unlimited) { 158 satirlar.push(` 159 <div class="usage-row"> 160 <div class="usage-label"> 161 <span class="name">${escapeHtml(tt('ui.set.assistant_messages', 'Asistan mesajları'))}</span>
162 <span class="value">${escapeHtml(String(kredi.used ?? 0))} · ${escapeHtml(tt('panel.fo_unlimited', 'Sınırsız'))}</span> 163 </div> 164 </div>`); 165 } else if (kredi.limit != null) { 166 const yuzde = Math.max(0, Math.min(100, Number(kredi.percent) || 0)); 167 satirlar.push(` 168 <div class="usage-row"> 169 <div class="usage-label"> 170 <span class="name">${escapeHtml(tt('ui.set.assistant_messages', 'Asistan mesajları'))}</span> 171 <span class="value">${escapeHtml(String(kredi.used ?? 0))} / ${escapeHtml(String(kredi.limit))}</span> 172 </div> 173 <div class="usage-bar"><div class="usage-fill${yuzde >= 90 ? ' warning' : ''}" style="width:${yuzde}%;"></div></div> 174 </div>`); 175 } 176 177 // Asistan tavanı yalnız katalog verdiyse yazılır; paydayı uydurmayız. 178 const acik = limits.public_assistants; 179 const ozel = limits.private_assistants; 180 if (acik != null || ozel != null) { 181 const p = []; 182 if (acik != null) p.push(`${escapeHtml(tt('panel.fo_public_assistants', 'Herkese açık'))}: ${escapeHtml(String(acik))}`); 183 if (ozel != null) p.push(`${escapeHtml(tt('panel.fo_private_assistants', 'Ãzel/davetli'))}: ${escapeHtml(String(ozel))}`); 184 satirlar.push(` 185 <div class="usage-row"> 186 <div class="usage-label"> 187 <span class="name">${escapeHtml(tt('panel.fo_assistant_limit', 'Asistan hakkınız'))}</span> 188 <span class="value">${p.join(' · ')}</span> 189 </div> 190 </div>`); 191 } 192 193 if (!satirlar.length) return ''; 194 const dnm = kredi.period ? ` <span style="color:var(--text-tertiary);">(${escapeHtml(kredi.period)})</span>` : ''; 195 return ` 196 <div class="setting-row stacked" style="border-bottom:0;padding-bottom:8px;"> 197 <div class="setting-info"> 198 <div class="setting-name">${escapeHtml(tt('ui.set.usage_month', 'Bu ay kullanım'))}${dnm}</div> 199 <div class="setting-desc">${escapeHtml(tt('panel.fo_usage_desc', 'Plan limitlerinize göre kullanımınız'))}</div> 200 </div> 201 </div> 202 ${satirlar.join('')} 203 <div style="height:6px;"></div>`; 204} 205 206function kartBolumu(kartlar) { 207 const ad = escapeHtml(tt('ui.set.payment_method', 'Ãdeme yöntemi')); 208 if (!kartlar.length) { 209 // Kart YOKSA bunu açıkça söyle. Eski ekran burada uydurma bir kart 210 // gösteriyordu ve kullanıcı hesabında kart tanımlı sanıyordu. 211 return satir(ad, escapeHtml(tt('panel.fo_no_card', 212 'Kayıtlı kartınız yok. Ãdeme sırasında kartınızı girer, dilerseniz sonraki ödemeler için kaydedebilirsiniz.')), ''); 213 } 214 const liste = kartlar.map(k => { 215 const skt = (k.month || k.year) ? ` · ${escapeHtml(k.month || '')}/${escapeHtml(k.year || '')}` : ''; 216 const banka = escapeHtml(k.bank || k.brand || ''); 217 return ` 218 <div class="setting-row" data-fo-card="${escapeHtml(k.ctoken)}"> 219 <div class="setting-info"> 220 <div class="setting-name">â¢â¢â¢â¢ ${escapeHtml(k.last_4 || '')}${banka ? ` · ${banka}` : ''}</div> 221 <div class="setting-desc">${escapeHtml(tt('panel.fo_saved_card', 'Kayıtlı kart'))}${skt}</div> 222 </div> 223 <div class="setting-control" data-fo-cardctl> 224 <button class="btn-secondary" data-fo-del="${escapeHtml(k.ctoken)}">${escapeHtml(tt('action.delete', 'Sil'))}</button> 225 </div> 226 </div>`; 227 }).join(''); 228 return ` 229 <div class="setting-row stacked" style="border-bottom:0;padding-bottom:4px;"> 230 <div class="setting-info"> 231 <div class="setting-name">${ad}</div> 232 <div class="setting-desc">${escapeHtml(tt('panel.fo_cards_desc', 233 'PayTR tarafında saklanan kartlarınız. Kart numarası Morfoz sunucularında tutulmaz.'))}</div> 234 </div> 235 </div> 236 ${liste}`; 237} 238 239function adresBolumu(adresler) { 240 const ad = escapeHtml(tt('ui.set.billing_address', 'Fatura adresi')); 241 const dugme = `<button class="btn-secondary" data-fo-addr>${escapeHtml( 242 adresler.length ? tt('action.edit', 'Düzenle') : tt('panel.fo_add_addr', 'Adres ekle'))}</button>`; 243 if (!adresler.length) { 244 return satir(ad, escapeHtml(tt('panel.fo_no_addr', 245 'Kayıtlı fatura adresiniz yok. Ãdeme sırasında istenir.')), dugme); 246 }
247 const a = adresler.find(x => x.is_default) || adresler[0]; 248 const p = [a.title || a.company_name || a.full_name, a.city, a.country] 249 .filter(Boolean).map(escapeHtml); 250 if (a.tax_id) p.push(`${escapeHtml(tt('panel.fo_tax_id', 'VKN'))}: ${escapeHtml(a.tax_id)}`); 251 const fazla = adresler.length > 1 252 ? ` <span style="color:var(--text-tertiary);">${escapeHtml( 253 tt('panel.fo_more_addr', '+{n} adres daha').replace('{n}', String(adresler.length - 1)))}</span>` 254 : ''; 255 return satir(ad, p.join(' · ') + fazla, dugme); 256} 257 258function faturaBolumu(siparisler) { 259 const ad = escapeHtml(tt('ui.set.invoice_history', 'Fatura geçmiÅi')); 260 const odenmis = (siparisler || []).filter(o => o && o.status === 'paid'); 261 if (!odenmis.length) { 262 return satir(ad, escapeHtml(tt('panel.fo_no_invoice', 'Henüz üyelik ödemeniz yok.')), ''); 263 } 264 const liste = odenmis.slice(0, 10).map(o => { 265 const tutar = para( 266 o.display_total_cents != null ? o.display_total_cents : o.total_cents, 267 o.display_currency || o.currency); 268 const no = o.invoice_no || o.order_no || `#${o.id}`; 269 return ` 270 <div class="setting-row"> 271 <div class="setting-info"> 272 <div class="setting-name">${escapeHtml(no)}</div> 273 <div class="setting-desc">${escapeHtml(fmtDate(o.paid_at || o.created_at))} 274 ${o.membership_tier ? ` · ${escapeHtml(String(o.membership_tier))}` : ''}</div> 275 </div> 276 <div class="setting-control"><span style="font-family:var(--mono);font-weight:600;">${escapeHtml(tutar)}</span></div> 277 </div>`; 278 }).join(''); 279 return ` 280 <div class="setting-row stacked" style="border-bottom:0;padding-bottom:4px;"> 281 <div class="setting-info"> 282 <div class="setting-name">${ad}</div> 283 <div class="setting-desc">${escapeHtml(tt('panel.fo_invoice_desc', 'Ãyelik ödemeleriniz'))}</div> 284 </div> 285 </div> 286 ${liste}`; 287} 288 289// Plan kataloÄu tek seferlik önbellek: aynı ekranda birkaç yerden soruluyor 290// (fatura paneli, baÅvuru kutusu). Dil deÄiÅince sıfırlanır â plan adı ve 291// kısıt satırlarını sunucu üretiyor. 292let _katalog = null; 293let _katalogDil = ''; 294 295async function katalogAl() { 296 const dil = currentLang() || 'tr'; 297 if (_katalog && _katalogDil === dil) return _katalog; 298 _katalog = await api.get('/api/memberships/tiers?lang=' + encodeURIComponent(dil)); 299 _katalogDil = dil; 300 return _katalog; 301} 302 303/** 304 * Bir planın SUNUCUDAKİ adı ve fiyatı. 305 * 306 * NEDEN: üyelik baÅvuru kutusunu açan satır-içi script fiyatları kendi 307 * içinde SABİT tutuyordu (`pro: 29.95`, `gold: 99.95`). Fiyat kartları 308 * API'den güncelleniyordu ama bu kutu güncellenmiyordu; admin panelinden 309 * fiyat deÄiÅtirildiÄinde kullanıcı baÅvuru ekranında ESKİ tutarı görüyordu. 310 * Ãstelik fiyat kullanıcının para biriminde geliyor â sabit "$" büsbütün 311 * yanlıÅtı. 312 * 313 * Bulunamazsa null döner ve çaÄıran kendi yedeÄinde kalır: fiyat ucu 314 * yavaÅladı diye baÅvuru kutusu açılmamazlık etmemeli. 315 */ 316export async function planBilgisi(tier) { 317 try { 318 const kat = await katalogAl(); 319 const p = (kat.tiers || []).find( 320 x => String(x.tier).toLowerCase() === String(tier).toLowerCase()); 321 if (!p) return null; 322 return { 323 name: p.name || tier, 324 priceText: (p.price && p.price.display_text) || '', 325 // `panel.mb_per_year` bölü iÅaretini İÃERİYOR ("/ yıl"); burada ayıracı 326 // kendimiz koyduÄumuz için bölüsüz anahtar kullanılır. 327 period: p.period === 'year' ? tt('panel.fo_per_year', 'yıl') 328 : tt('ui.mb.per_month_short', 'ay'), 329 }; 330 } catch { return null; } 331} 332 333/** 334 * Panelim (dashboard) â "Plan & kullanım" kartı. 335 * 336 * Aynı prototip kusuru buradaydı ve DAHA görünürdü: giriÅ yapan herkes ilk 337 * ekranda "Plus", "3.184 / 5.000 mesaj", "%100 dolu asistan", "142 MB / 1 GB" 338 * ve "Yenileme: 14 Haz 2026" görüyordu. Hiçbiri ölçülmüŠdeÄildi. 339 * 340 * Depolama ve API çaÄrısı satırları geri gelmedi: ikisini de ölçen bir uç yok 341 * ve bu ekranın tüm sorunu zaten ölçülmemiÅ sayı göstermekti. 342 */ 343export async function panoPlanKartiniCiz() { 344 const host = document.getElementById('dashPlanHost'); 345 if (!host) return; 346 const sonuc = await Promise.allSettled([ 347 api.get('/api/memberships/credits'), 348 api.get('/api/memberships/active'), 349 ]); 350 const kredi = sonuc[0].status === 'fulfilled' ? sonuc[0].value : null; 351 const active = (sonuc[1].status === 'fulfilled' ? sonuc[1].value : null) || {}; 352 353 const slug = String(active.tier || 'free').toLowerCase(); 354 const pill = document.getElementById('dashPlanPill'); 355 if (pill) pill.textContent = slug.charAt(0).toUpperCase() + slug.slice(1); 356 const foot = document.getElementById('dashPlanRenew'); 357 if (foot) { 358 foot.textContent = active.period_end 359 ? (String(active.status) === 'cancelling' 360 ? tt('panel.fo_period_end', 'EriÅim bitiÅi: {d}') 361 : tt('panel.fo_next_renew', 'Sonraki yenileme: {d}') 362 ).replace('{d}', gun(active.period_end)) 363 : ''; 364 } 365 366 const satirlar = []; 367 if (kredi && kredi.unlimited) { 368 satirlar.push(` 369 <div class="dash-usage-row"> 370 <div class="dash-usage-label">
371 <span class="name">${escapeHtml(tt('panel.fo_msg_limit', 'Mesaj limiti'))}</span> 372 <span class="value">${escapeHtml(String(kredi.used ?? 0))} · ${escapeHtml(tt('panel.fo_unlimited', 'Sınırsız'))}</span> 373 </div> 374 </div>`); 375 } else if (kredi && kredi.limit != null) { 376 const y = Math.max(0, Math.min(100, Number(kredi.percent) || 0)); 377 satirlar.push(` 378 <div class="dash-usage-row"> 379 <div class="dash-usage-label"> 380 <span class="name">${escapeHtml(tt('panel.fo_msg_limit', 'Mesaj limiti'))}</span> 381 <span class="value">${escapeHtml(String(kredi.used ?? 0))} / ${escapeHtml(String(kredi.limit))}</span> 382 </div> 383 <div class="dash-usage-bar"><div class="fill${y >= 90 ? ' warning' : ''}" style="width:${y}%;"></div></div> 384 </div>`); 385 } 386 387 // Yükseltme çaÄrısı YALNIZ yükseltilecek bir Åey varken. Eskiden Gold 388 // kullanıcısına da "Pro'ya yükseltin" yazıyordu. 389 const yukselt = (slug === 'free' || slug === 'plus') ? ` 390 <div style="margin-top:16px;padding:12px 14px;background:var(--accent-soft);border-radius:10px; 391 display:flex;align-items:center;gap:10px;"> 392 <div style="flex:1;font-size:11.5px;color:var(--text-primary);line-height:1.4;"> 393 <strong>${escapeHtml(tt('ui.dash.upgrade_title', "Pro'ya yükseltin:"))}</strong> 394 ${escapeHtml(tt('panel.fo_upgrade_desc', 'Daha yüksek mesaj hakkı ve daha fazla asistan.'))} 395 </div> 396 </div>` : ''; 397 398 host.innerHTML = satirlar.join('') + yukselt; 399} 400 401/** 402 * Ãyelik baÅvuru kutusundaki ödeme seçenekleri (#mbPayHost). 403 * 404 * Buradaki radyo düÄmesini panel-app.js okuyup sunucuya `payment_method` 405 * olarak gönderiyor â yani süs deÄil. Eski hâli sabit iki seçenekti: 406 * "$129.15 bakiye mevcut" ve "Kayıtlı kart: **** 4216". İkisi de uydurmaydı; 407 * üstelik cüzdan bu kurulumda KAPALI olduÄu hâlde varsayılan seçili seçenek 408 * oydu ve sunucu böyle bir baÅvuruyu reddediyordu. 409 * 410 * HER KOÅULDA bir seçenek iÅaretli çizilir: panel-app.js hiçbiri seçili 411 * deÄilse 'wallet'a düÅüyor, bu da cüzdan kapalıyken kesin hata demek. 412 */ 413export async function odemeSecenekleriniCiz() { 414 const host = document.getElementById('mbPayHost'); 415 if (!host) return; 416 const cuzdanAcik = !!(window.morfozConfig || {}).wallet_enabled; 417 418 let bakiye = null; 419 if (cuzdanAcik) { 420 try { 421 const w = await api.get('/api/wallet/balance'); 422 bakiye = w && (w.balance_cents != null ? w.balance_cents : w.balance); 423 } catch { bakiye = null; } 424 } 425 let kartlar = []; 426 try { 427 kartlar = (await api.get('/api/paytr/cards')).cards || []; 428 } catch { kartlar = []; } 429 430 const KUTU = 'display:flex;align-items:center;gap:10px;padding:12px;' 431 + 'border-radius:10px;cursor:pointer;'; 432 const secili = cuzdanAcik ? 'wallet' : 'card'; 433 const parcalar = []; 434 435 if (cuzdanAcik) { 436 const bakiyeMetni = bakiye != null 437 ? para(bakiye, 'USD') 438 : tt('panel.fo_balance_unknown', 'bakiye okunamadı'); 439 parcalar.push(` 440 <label style="${KUTU}border:2px solid var(--brand);background:var(--accent-soft);"> 441 <input type="radio" name="mbPay" value="wallet" checked style="margin:0;"> 442 <div style="flex:1;"> 443 <div style="font-weight:500;font-size:13px;">${escapeHtml(tt('ui.mb.pay_wallet', 'Cüzdandan'))}</div> 444 <div style="font-size:11.5px;color:var(--text-secondary);">${escapeHtml( 445 tt('panel.fo_balance', 'Bakiye: {b}').replace('{b}', bakiyeMetni))}</div> 446 </div> 447 </label>`); 448 } 449 450 const kartAlt = kartlar.length 451 ? kartlar.map(k => `â¢â¢â¢â¢ ${escapeHtml(k.last_4 || '')}`).join(', ') 452 : escapeHtml(tt('panel.fo_card_at_checkout', 453 'Ãdeme adımında kart bilgilerinizi gireceksiniz.')); 454 parcalar.push(` 455 <label style="${KUTU}border:${secili === 'card' ? '2px solid var(--brand)' : '1px solid var(--border)'};${ 456 secili === 'card' ? 'background:var(--accent-soft);' : ''}"> 457 <input type="radio" name="mbPay" value="card"${secili === 'card' ? ' checked' : ''} style="margin:0;"> 458 <div style="flex:1;"> 459 <div style="font-weight:500;font-size:13px;">${escapeHtml(tt('ui.mb.pay_card', 'Kredi kartı'))}</div> 460 <div style="font-size:11.5px;color:var(--text-secondary);">${kartAlt}</div> 461 </div> 462 </label>`); 463 464 host.innerHTML = `<div style="display:flex;flex-direction:column;gap:8px;">${parcalar.join('')}</div>`; 465} 466 467function bagla(host, kartlar) { 468 host.querySelector('[data-fo-plans]')?.addEventListener('click', () => { 469 document.querySelector('.rail-btn[data-nav="membership"]')?.click(); 470 }); 471 host.querySelector('[data-fo-addr]')?.addEventListener('click', () => { 472 // Adres defteri Hesabım â Adreslerim'de yaÅıyor; ikinci bir düzenleyici 473 // açmak iki ayrı doÄrulama kuralı demek olurdu. 474 document.querySelector('.rail-btn[data-nav="account"]')?.click(); 475 setTimeout(() => { 476 document.querySelector('#accountNav .settings-nav-item[data-apane="addresses"]')?.click(); 477 }, 80); 478 }); 479 480 host.querySelectorAll('[data-fo-del]').forEach(btn => { 481 btn.addEventListener('click', () => kartSilOnayi(btn, host)); 482 }); 483 void kartlar; 484} 485
486/** 487 * Kart silme onayı SATIR İÃİNDE sorulur â `confirm()` KULLANMIYORUZ. 488 * 489 * Bu depoda tarayıcı kutularının bastırılması akıÅları sessizce öldürdü: 490 * confirm() bastırıldıÄında anında `false` döner, hiçbir istek gitmez ve 491 * kullanıcı hiçbir hata görmez. Ãyelik iptali tam olarak böyle "çalıÅmıyor" 492 * görünmüÅtü. Onay artık DOM'da. 493 */ 494function kartSilOnayi(btn, host) { 495 const ctoken = btn.dataset.foDel; 496 const kutu = btn.closest('[data-fo-cardctl]'); 497 if (!kutu) return; 498 const eski = kutu.innerHTML; 499 kutu.innerHTML = ` 500 <span style="font-size:11.5px;color:var(--text-secondary);margin-right:6px;">${escapeHtml( 501 tt('panel.fo_del_confirm', 'Kart silinsin mi?'))}</span> 502 <button class="btn-danger" data-fo-yes>${escapeHtml(tt('action.yes', 'Evet'))}</button> 503 <button class="btn-secondary" data-fo-no>${escapeHtml(tt('action.cancel', 'Vazgeç'))}</button>`; 504 kutu.querySelector('[data-fo-no]')?.addEventListener('click', () => { kutu.innerHTML = eski; 505 kutu.querySelector('[data-fo-del]')?.addEventListener('click', e => kartSilOnayi(e.currentTarget, host)); }); 506 kutu.querySelector('[data-fo-yes]')?.addEventListener('click', async e => { 507 const ev = e.currentTarget; 508 ev.disabled = true; 509 ev.textContent = tt('panel.fo_deleting', 'Siliniyorâ¦'); 510 try { 511 await api.del(`/api/paytr/cards/${encodeURIComponent(ctoken)}`); 512 toast(tt('panel.fo_card_deleted', 'Kart silindi'), 'success'); 513 await faturaOdemeCiz(); 514 } catch (err) { 515 toast((err && err.message) || tt('panel.fo_card_del_fail', 'Kart silinemedi'), 'error'); 516 kutu.innerHTML = eski; 517 kutu.querySelector('[data-fo-del]')?.addEventListener('click', x => kartSilOnayi(x.currentTarget, host)); 518 } 519 }); 520}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.