1// ============================================================ 2// Admin - Tüm Cüzdanlar (wallet accounts overview) 3// ============================================================ 4// API: 5// GET /api/admin/wallet/accounts?anomaliesOnly=false 6// GET /api/admin/wallet/accounts/{userId}/transactions 7// POST /api/admin/wallet/manual-debit { userId, amount, description?, referenceId? } (Issue #2057, madde C) 8// ============================================================ 9(function () { 10 'use strict'; 11 12 let allItems = []; 13 let activeFilter = 'all'; 14 let searchTerm = ''; 15 let bsDetailModal = null; 16 let bsDebitModal = null; 17 let debitTargetUserId = null; 18 let debitSubmitInFlight = false; 19 20 function $(id) { return document.getElementById(id); } 21 22 function escapeHtml(s) { 23 if (s == null) return ''; 24 return String(s) 25 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') 26 .replace(/"/g, '"').replace(/'/g, '''); 27 } 28 29 function formatTry(amount) { 30 if (amount == null || isNaN(amount)) return '-'; 31 return window.BinderimFormat.formatNumber(Number(amount), { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' âº'; 32 } 33 34 // *Utc alanları UTC'dir ama JSON'a offset'siz yazılır ("2026-06-15T20:02:00"). Bu string'i 35 // new Date() YEREL saat sayar â TR'de saatler 3 saat geri görünür. Offset yoksa 'Z' ekle. 36 function parseUtc(iso) { 37 if (!iso) return null; 38 const normalized = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(iso) ? iso : iso + 'Z'; 39 const d = new Date(normalized); 40 return isNaN(d.getTime()) ? null : d; 41 } 42 43 function formatDate(iso) { 44 const d = parseUtc(iso); 45 if (!d) return '-'; 46 return window.BinderimFormat.formatDateTime(d, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); 47 } 48 49 function getCsrfToken() { 50 const el = $('waAntiforgeryToken'); 51 return el ? el.value : null; 52 } 53 54 async function fetchJson(url, init) { 55 const opts = init || {}; 56 opts.headers = Object.assign({ 'Accept': 'application/json' }, opts.headers || {}); 57 if (opts.body && !(opts.body instanceof FormData)) { 58 opts.headers['Content-Type'] = 'application/json'; 59 } 60 const tok = getCsrfToken(); 61 if (tok) opts.headers['RequestVerificationToken'] = tok; 62 opts.credentials = 'same-origin'; 63 const res = await fetch(url, opts); 64 let payload = null; 65 try { payload = await res.json(); } catch (e) { /* ignore */ } 66 if (!res.ok || (payload && payload.success === false)) { 67 const msg = (payload && (payload.message || payload.errorMessage)) || ('HTTP ' + res.status); 68 throw new Error(msg); 69 } 70 return payload; 71 } 72 73 // ---------------- Summary ----------------
74 function renderSummary(s) { 75 if (!s) return; 76 $('waSumWallets').textContent = s.walletCount != null ? s.walletCount : '-'; 77 $('waSumAvailable').textContent = formatTry(s.totalAvailableBalance); 78 $('waSumTopUp').textContent = formatTry(s.totalTopUp); 79 $('waSumKargonomi').textContent = formatTry(s.totalKargonomiSpend); 80 $('waSumAnomaly').textContent = s.anomalyCount != null ? s.anomalyCount : '-'; 81 82 const badge = $('waAnomalyBadge'); 83 if (badge) { 84 const count = s.anomalyCount || 0; 85 badge.textContent = count + ' anomali'; 86 badge.classList.toggle('bd-badge--danger', count > 0); 87 badge.classList.toggle('bd-badge--success', count === 0); 88 } 89 } 90 91 // ---------------- Table ---------------- 92 function statusCell(it) { 93 if (it.hasAnomaly) { 94 return '<span class="wa-pill wa-pill--anomaly"><i class="bi bi-exclamation-triangle-fill"></i> Anomali</span>'; 95 } 96 if (!it.hasWallet) { 97 return '<span class="wa-pill wa-pill--nowallet">Cüzdan yok</span>'; 98 } 99 if (!it.isActive) { 100 return '<span class="wa-pill wa-pill--inactive">Pasif</span>'; 101 } 102 return '<span class="wa-pill wa-pill--ok"><i class="bi bi-check-circle-fill"></i> Normal</span>'; 103 } 104 105 function filteredItems() { 106 let items = allItems; 107 if (activeFilter === 'anomalies') { 108 items = items.filter(function (x) { return x.hasAnomaly; }); 109 } 110 if (searchTerm) { 111 const t = searchTerm.toLowerCase(); 112 items = items.filter(function (x) { 113 return (x.displayName || '').toLowerCase().indexOf(t) !== -1 114 || (x.email || '').toLowerCase().indexOf(t) !== -1; 115 }); 116 } 117 return items; 118 } 119 120 function renderTable() { 121 const tbody = $('waTableBody'); 122 if (!tbody) return; 123 const items = filteredItems(); 124 if (items.length === 0) { 125 tbody.innerHTML = '<tr><td colspan="8" class="wa-empty">Kayıt bulunamadı.</td></tr>'; 126 return; 127 } 128 129 tbody.innerHTML = items.map(function (it) { 130 const vendor = '<div class="wa-vendor">' 131 + '<span class="wa-vendor__name">' + escapeHtml(it.displayName) + '</span>' 132 + (it.email ? '<span class="wa-vendor__email">' + escapeHtml(it.email) + '</span>' : '') 133 + '</div>'; 134 135 const spendClass = it.netKargonomiSpend > 0 ? 'wa-spend wa-spend--has' : 'wa-spend wa-muted'; 136 // #1726: vurgu artik SAYMA farkina degil, gonderi-kesinti ESLESTIRMESINE bakar. 137 // (labelCount > shippingDebitCount karsilastirmasi yanlis negatif uretiyordu: bir 138 // etiketin LIFO drawdown ile 2 satira bolunmesi, hic kesilmemis baska bir etiketi 139 // maskeliyordu.) 140 const countMismatch = (it.uncoveredLabelCount || 0) > 0; 141 const countsClass = countMismatch ? 'wa-counts wa-counts--mismatch' : 'wa-counts'; 142 143 return '<tr class="' + (it.hasAnomaly ? 'wa-row--anomaly' : '') + '" tabindex="0" role="button" data-userid="' + escapeHtml(it.userId) + '">' 144 + '<td>' + vendor + '</td>' 145 + '<td class="wa-num">' + formatTry(it.availableBalance) + '</td>' 146 + '<td class="wa-num">' + (it.heldBalance > 0 ? formatTry(it.heldBalance) : '<span class="wa-muted">-</span>') + '</td>' 147 + '<td class="wa-num">' + formatTry(it.totalFunded) + '</td>' 148 + '<td class="wa-num"><span class="' + spendClass + '">' + formatTry(it.netKargonomiSpend) + '</span></td>' 149 + '<td class="wa-num"><span class="' + countsClass + '">' + it.labelCount + ' / ' + it.shippingDebitCount + '</span></td>' 150 + '<td>' + statusCell(it) + '</td>' 151 + '<td class="wa-num"><i class="bi bi-chevron-right wa-muted"></i></td>' 152 + '</tr>'; 153 }).join(''); 154 155 Array.prototype.forEach.call(tbody.querySelectorAll('tr[data-userid]'), function (tr) { 156 tr.addEventListener('click', function () { 157 openDetail(tr.getAttribute('data-userid')); 158 }); 159 tr.addEventListener('keydown', function (e) { 160 if (e.key === 'Enter' || e.key === ' ') { 161 e.preventDefault(); 162 openDetail(tr.getAttribute('data-userid')); 163 } 164 }); 165 }); 166 } 167 168 // ---------------- Detail modal ---------------- 169 function dirChip(direction) { 170 const d = (direction || '').toLowerCase(); 171 if (d === 'credit') return '<span class="wa-dir--credit">+ Alacak</span>'; 172 if (d === 'debit') return '<span class="wa-dir--debit">- Borç</span>'; 173 return escapeHtml(direction); 174 } 175 176 function txTypeLabel(t) { 177 return ({ 178 TopUp: 'Bakiye Yükleme', 179 Adjustment: 'Manuel Düzeltme', 180 ShippingDebit: 'Kargo Kesintisi', 181 // Issue #1717 - kargo kesintisi emanet rayindan da yapilabilir; bu iki tip eskiden ham 182 // (İngilizce) gorunuyordu. Diger escrow tipleri (EscrowFundingCredit, EscrowReleaseCredit, 183 // SaleCommissionDebit, MarketplacePurchaseDebit, ...) bu task'in kapsami disinda BILEREK 184 // eklenmedi - degistirirsen WalletShippingSpendTypes.cs'teki notu da guncelle. 185 EscrowShippingDebit: 'Kargo Kesintisi (Emanet)', 186 EscrowShippingRefundCredit: 'Kargo İadesi (Emanet)', 187 Refund: 'İade', 188 Hold: 'Bloke', 189 Release: 'Bloke Ãözüldü', 190 Capture: 'Tahsilat', 191 Settlement: 'Mahsup' 192 })[t] || t; 193 } 194 195 async function openDetail(userId) { 196 const item = allItems.find(function (x) { return x.userId === userId; }); 197 debitTargetUserId = userId; 198 $('waModalUserLabel').textContent = item ? item.displayName : userId; 199 $('waDetailContent').innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary"></div></div>'; 200 201 const anomalyBox = $('waDetailAnomaly'); 202 if (item && item.hasAnomaly && item.anomalies && item.anomalies.length) { 203 anomalyBox.innerHTML = '<strong><i class="bi bi-exclamation-triangle-fill"></i> Tespit edilen tutarsızlıklar:</strong>' 204 + '<ul class="wa-detail-anomaly-list">' 205 + item.anomalies.map(function (a) { return '<li>' + escapeHtml(a) + '</li>'; }).join('') 206 + '</ul>'; 207 anomalyBox.classList.remove('d-none'); 208 } else { 209 anomalyBox.classList.add('d-none'); 210 anomalyBox.innerHTML = ''; 211 } 212 213 if (!bsDetailModal) { 214 bsDetailModal = new bootstrap.Modal($('waDetailModal')); 215 } 216 bsDetailModal.show(); 217 218 try { 219 const payload = await fetchJson('/api/admin/wallet/accounts/' + encodeURIComponent(userId) + '/transactions'); 220 renderDetail(payload.data); 221 } catch (e) { 222 $('waDetailContent').innerHTML = '<div class="alert alert-danger">' + escapeHtml(e.message) + '</div>'; 223 } 224 } 225 226 function renderDetail(data) { 227 if (!data) { 228 $('waDetailContent').innerHTML = '<div class="wa-empty">Veri yok.</div>'; 229 return; 230 } 231 232 let html = ''; 233 234 // Kargonomi gönderileri (KargonomiShipmentId bazında gruplu) 235 const labels = data.labels || []; 236 html += '<div class="wa-section-title"><i class="bi bi-box-seam"></i> Kargonomi Gönderileri (' + labels.length + ')</div>'; 237 if (labels.length === 0) { 238 html += '<div class="wa-empty">Kargonomi gönderisi yok.</div>'; 239 } else { 240 html += '<table class="wa-tx-table"><thead><tr>' 241 + '<th>SipariÅ</th><th>Kargonomi ID</th><th class="wa-num">Ãrün Sayısı</th><th>Durum</th><th class="wa-num">Kargo Ãcreti</th>' 242 + '</tr></thead><tbody>' 243 + labels.map(function (l) { 244 return '<tr>' 245 + '<td>#' + escapeHtml(l.orderId) + '</td>' 246 + '<td>' + escapeHtml(l.kargonomiShipmentId) + '</td>' 247 + '<td class="wa-num">' + escapeHtml(l.itemCount) + '</td>' 248 + '<td>' + escapeHtml(l.kargonomiShipmentStatusLabel || '-') + '</td>' 249 + '<td class="wa-num">' + formatTry(l.shippingPrice) + '</td>' 250 + '</tr>'; 251 }).join('') 252 + '</tbody></table>'; 253 } 254 255 // Cüzdan hareketleri 256 const txs = data.transactions || []; 257 html += '<div class="wa-section-title"><i class="bi bi-list-ul"></i> Cüzdan Hareketleri (' + txs.length + ')</div>'; 258 if (!data.hasWallet) { 259 html += '<div class="alert alert-warning mb-0">Bu kullanıcının cüzdan hesabı yok.</div>'; 260 } else if (txs.length === 0) { 261 html += '<div class="wa-empty">Hiç cüzdan hareketi yok.</div>'; 262 } else { 263 html += '<table class="wa-tx-table"><thead><tr>' 264 + '<th>Tarih</th><th>Tür</th><th>Yön</th><th class="wa-num">Tutar</th><th class="wa-num">Bakiye Sonrası</th><th>Açıklama</th>' 265 + '</tr></thead><tbody>' 266 + txs.map(function (t) { 267 return '<tr>' 268 + '<td>' + formatDate(t.createdUtc) + '</td>' 269 + '<td>' + escapeHtml(txTypeLabel(t.transactionType)) + '</td>' 270 + '<td>' + dirChip(t.direction) + '</td>' 271 + '<td class="wa-num">' + formatTry(t.amount) + '</td>' 272 + '<td class="wa-num">' + formatTry(t.balanceAfter) + '</td>' 273 + '<td>' + escapeHtml(t.description || '-') + '</td>' 274 + '</tr>'; 275 }).join('') 276 + '</tbody></table>'; 277 } 278 279 $('waDetailContent').innerHTML = html; 280 } 281 282 // ---------------- Manuel bakiye düÅümü (Issue #2057, madde C) ---------------- 283 function hideDebitAlert() { 284 const box = $('waDebitAlert'); 285 if (box) { 286 box.classList.add('d-none'); 287 box.innerHTML = ''; 288 } 289 } 290 291 function showDebitAlert(kind, message) { 292 const box = $('waDebitAlert'); 293 if (!box) return; 294 box.className = 'alert alert-' + kind; 295 box.textContent = message; 296 } 297 298 function resetDebitModal() { 299 hideDebitAlert(); 300 const amountEl = $('waDebitAmount'); 301 if (amountEl) amountEl.value = ''; 302 const refEl = $('waDebitReferenceId'); 303 if (refEl) refEl.value = ''; 304 const descEl = $('waDebitDescription'); 305 if (descEl) descEl.value = ''; 306 const submitBtn = $('waDebitSubmitBtn'); 307 if (submitBtn) { 308 submitBtn.disabled = false;
309 submitBtn.innerHTML = '<i class="bi bi-dash-circle"></i> Bakiyeden DüÅ'; 310 } 311 debitSubmitInFlight = false; 312 } 313 314 function openDebitModal() { 315 if (!debitTargetUserId) return; 316 const item = allItems.find(function (x) { return x.userId === debitTargetUserId; }); 317 $('waDebitUserLabel').textContent = item ? item.displayName : debitTargetUserId; 318 resetDebitModal(); 319 if (!bsDebitModal) { 320 bsDebitModal = new bootstrap.Modal($('waDebitModal')); 321 } 322 bsDebitModal.show(); 323 setTimeout(function () { 324 const amountEl = $('waDebitAmount'); 325 if (amountEl) amountEl.focus(); 326 }, 50); 327 } 328 329 async function submitDebit() { 330 if (debitSubmitInFlight || !debitTargetUserId) return; 331 332 const amountEl = $('waDebitAmount'); 333 const rawAmount = amountEl ? (amountEl.value || '').trim() : ''; 334 const amount = window.BinderimFormat.parseInput(amountEl); 335 if (!rawAmount || isNaN(amount) || amount <= 0) { 336 showDebitAlert('warning', 'Geçerli bir tutar girin.'); 337 if (amountEl) amountEl.focus(); 338 return; 339 } 340 341 const refEl = $('waDebitReferenceId'); 342 const referenceId = refEl ? (refEl.value || '').trim() : ''; 343 const descEl = $('waDebitDescription'); 344 const description = descEl ? (descEl.value || '').trim() : ''; 345 346 debitSubmitInFlight = true; 347 hideDebitAlert(); 348 const submitBtn = $('waDebitSubmitBtn'); 349 if (submitBtn) { 350 submitBtn.disabled = true; 351 submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> İÅleniyor...'; 352 } 353 354 try { 355 const res = await fetchJson('/api/admin/wallet/manual-debit', { 356 method: 'POST', 357 body: JSON.stringify({ 358 userId: debitTargetUserId, 359 amount: amount, 360 description: description || null, 361 referenceId: referenceId || null 362 }) 363 }); 364 const msg = (res && res.message) || 'Bakiyeden düÅüldü.'; 365 showDebitAlert('success', msg); 366 await load(); 367 if (debitTargetUserId) { 368 await openDetail(debitTargetUserId); 369 } 370 setTimeout(function () { 371 if (bsDebitModal) bsDebitModal.hide(); 372 }, 1400); 373 } catch (e) { 374 showDebitAlert('danger', e.message || 'Bakiyeden düÅülemedi.'); 375 if (submitBtn) { 376 submitBtn.disabled = false; 377 submitBtn.innerHTML = '<i class="bi bi-dash-circle"></i> Bakiyeden DüÅ'; 378 } 379 } finally { 380 debitSubmitInFlight = false; 381 } 382 } 383 384 // ---------------- Load ---------------- 385 async function load() { 386 const tbody = $('waTableBody'); 387 if (tbody) tbody.innerHTML = '<tr><td colspan="8" class="wa-empty">Yükleniyor...</td></tr>'; 388 try { 389 const url = activeFilter === 'anomalies' 390 ? '/api/admin/wallet/accounts?anomaliesOnly=true' 391 : '/api/admin/wallet/accounts'; 392 const payload = await fetchJson(url); 393 const data = payload.data || {}; 394 allItems = data.items || []; 395 renderSummary(data.summary); 396 renderTable(); 397 } catch (e) { 398 if (tbody) tbody.innerHTML = '<tr><td colspan="8" class="wa-empty">Hata: ' + escapeHtml(e.message) + '</td></tr>'; 399 } 400 } 401 402 // ---------------- Wiring ---------------- 403 document.addEventListener('DOMContentLoaded', function () { 404 $('waRefreshBtn').addEventListener('click', load); 405 406 const strip = $('waFilterStrip'); 407 if (strip) { 408 strip.addEventListener('click', function (e) { 409 const btn = e.target.closest('button[data-filter]'); 410 if (!btn) return; 411 const prevFilter = activeFilter; 412 activeFilter = btn.getAttribute('data-filter'); 413 Array.prototype.forEach.call(strip.querySelectorAll('button'), function (b) { 414 b.classList.toggle('active', b === btn); 415 }); 416 // Re-fetch when switching to/from anomalies so the API can filter server-side 417 if (activeFilter === 'anomalies' || prevFilter === 'anomalies') { 418 load(); 419 } else { 420 renderTable(); 421 } 422 }); 423 } 424 425 const search = $('waSearchInput'); 426 if (search) { 427 search.addEventListener('input', function () { 428 searchTerm = search.value.trim(); 429 renderTable(); 430 }); 431 } 432
433 const openDebitBtn = $('waOpenDebitBtn'); 434 if (openDebitBtn) openDebitBtn.addEventListener('click', openDebitModal); 435 const debitSubmitBtn = $('waDebitSubmitBtn'); 436 if (debitSubmitBtn) debitSubmitBtn.addEventListener('click', submitDebit); 437 const debitModalEl = $('waDebitModal'); 438 if (debitModalEl) debitModalEl.addEventListener('hidden.bs.modal', resetDebitModal); 439 440 load(); 441 }); 442})();
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.