PageSourceSearch

https://morfoz.ai/public/fatura-odeme.js?v=1790769371

js morfoz.ai collected 2026-10-05 15:11:00 UTC 23,181 bytes, 520 lines download raw bytes

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.