1(function () { 2 'use strict'; 3 4 var $ = function (id) { return document.getElementById(id); }; 5 var apiBase = '/api/admin/wallet/liability'; 6 7 function antiforgeryToken() { 8 var el = $('wlAntiforgeryToken'); 9 return el ? el.value : ''; 10 } 11 12 function fmtTry(value) { 13 var n = Number(value || 0); 14 return window.BinderimFormat.formatNumber(n, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' âº'; 15 } 16 17 // *Utc alanları UTC'dir ama JSON'a offset'siz yazılır ("2026-06-15T20:02:00"). Bu string'i 18 // new Date() YEREL saat sayar â TR'de saatler 3 saat geri görünür. Offset yoksa 'Z' ekle. 19 // NOT: aÅaÄıdaki toDateInputValue/filtre varsayılanları BİLEREK yerel â kullanıcının seçtiÄi 20 // takvim günü, sunucunun UTC günü deÄil. 21 function parseUtc(iso) { 22 if (!iso) return null; 23 var normalized = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(iso) ? iso : iso + 'Z'; 24 var d = new Date(normalized); 25 return isNaN(d.getTime()) ? null : d; 26 } 27 28 function fmtDate(iso) { 29 var d = parseUtc(iso); 30 if (!d) return '-'; 31 return window.BinderimFormat.formatDateTime(d, { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); 32 } 33 34 function toDateInputValue(d) { 35 var y = d.getFullYear(); 36 var m = String(d.getMonth() + 1).padStart(2, '0'); 37 var day = String(d.getDate()).padStart(2, '0'); 38 return y + '-' + m + '-' + day; 39 } 40 41 async function apiGet(path) { 42 var res = await fetch(apiBase + path, { credentials: 'same-origin' }); 43 var json = await res.json().catch(function () { return null; }); 44 if (!res.ok || !json || json.success === false) { 45 var msg = (json && (json.errorMessage || json.message)) || ('İstek baÅarısız (' + res.status + ')'); 46 throw new Error(msg); 47 } 48 return json.data; 49 } 50 51 async function apiPost(path) { 52 var res = await fetch(apiBase + path, { 53 method: 'POST', 54 credentials: 'same-origin', 55 headers: { 'RequestVerificationToken': antiforgeryToken() } 56 }); 57 var json = await res.json().catch(function () { return null; }); 58 if (!res.ok || !json || json.success === false) { 59 var msg = (json && (json.errorMessage || json.message)) || ('İstek baÅarısız (' + res.status + ')'); 60 throw new Error(msg); 61 } 62 return json.data; 63 } 64 65 function currentPeriod() { 66 var start = $('wlPeriodStart').value; 67 var end = $('wlPeriodEnd').value; 68 var qs = ''; 69 if (start) qs += '&periodStart=' + encodeURIComponent(start); 70 if (end) qs += '&periodEnd=' + encodeURIComponent(end); 71 return qs; 72 } 73 74 function initDefaultPeriod() { 75 var now = new Date(); 76 var firstOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); 77 $('wlPeriodStart').value = toDateInputValue(firstOfMonth); 78 $('wlPeriodEnd').value = toDateInputValue(now); 79 } 80 81 function badgeClassForStatus(isBreach) { 82 return isBreach ? 'bd-badge bd-badge--danger' : 'bd-badge bd-badge--success'; 83 } 84 85 function badgeClassForStripe(status) { 86 switch (status) { 87 case 'Ok': return 'bd-badge bd-badge--success'; 88 case 'Warning': return 'bd-badge bd-badge--warn'; 89 case 'Breach': return 'bd-badge bd-badge--danger'; 90 default: return 'bd-badge bd-badge--info'; 91 } 92 } 93 94 function renderReport(report) { 95 // Reviewer M1 paketi (PR #1445): K1/K2/K3 "Åimdiki durum" (dönem seçiminden baÄımsız) â 96 // Kontroller kartındaki alt yazı üretim zamanını gösterir, arÅivden bakan biri yanılmasın. 97 $('wlGeneratedUtc').textContent = fmtDate(report.generatedUtc); 98 99 $('wlOpeningValue').textContent = fmtTry(report.openingLiability); 100 $('wlClosingValue').textContent = fmtTry(report.closingLiability); 101 $('wlAccountsValue').textContent = fmtTry(report.accountsBalanceTotal); 102 $('wlHeldValue').textContent = fmtTry(report.heldTotalAtPeriodEnd); 103 $('wlOverrunValue').textContent = fmtTry(report.receiptOverrun); 104 $('wlCoverageValue').textContent = (report.receiptCoveragePercent != null) 105 ? ('Kapsama: %' + Number(report.receiptCoveragePercent).toFixed(1)) 106 : ''; 107 $('wlOpenWithdrawalsValue').textContent = report.openWithdrawalRequestCount; 108 109 // T-20260806, Epic #1479, Faz-1 (Issue #1480, Karar 4): emanet bilgi kartı + negatif bakiye 110 // "Alacak" bloÄu â Faz-1'de her ikisi de normalde 0/gizli (üretim çaÄıranı yok). 111 var escrowValueEl = $('wlEscrowValue'); 112 if (escrowValueEl) escrowValueEl.textContent = fmtTry(report.escrowTotal); 113 114 // T-20260806, Epic #1479, Faz-5 (Issue #1484, Karar 3/9) â donmuÅ tutar + teminatsız maruziyet. 115 var disputeFrozenEl = $('wlDisputeFrozenValue'); 116 if (disputeFrozenEl) disputeFrozenEl.textContent = fmtTry(report.disputeFrozenTotal); 117 var disputeUnsecuredEl = $('wlDisputeUnsecuredValue'); 118 if (disputeUnsecuredEl) disputeUnsecuredEl.textContent = fmtTry(report.disputeUnsecuredExposure); 119 120 // T-20260807, Epic #1479, Faz-5 (Issue #1484, Architect Rev-4/6) â platform iÅlem ücreti kaybı.
121 var platformFeeLossEl = $('wlPlatformFeeLossValue'); 122 if (platformFeeLossEl) platformFeeLossEl.textContent = fmtTry(report.platformFeeLoss); 123 124 var negBanner = $('wlNegativeBalanceBanner'); 125 if (negBanner) { 126 if (report.negativeBalanceAccountCount > 0) { 127 negBanner.classList.remove('d-none'); 128 $('wlNegativeBalanceAccountCount').textContent = report.negativeBalanceAccountCount; 129 $('wlNegativeBalanceTotal').textContent = fmtTry(report.negativeBalanceTotal); 130 $('wlGrossLiabilityValue').textContent = fmtTry(report.grossLiability); 131 } else { 132 negBanner.classList.add('d-none'); 133 } 134 } 135 136 var unclassifiedBanner = $('wlUnclassifiedBanner'); 137 if (report.unclassifiedTransactionCount > 0) { 138 unclassifiedBanner.classList.remove('d-none'); 139 $('wlUnclassifiedText').textContent = 140 report.unclassifiedTransactionCount + ' adet sınıflandırılmamıŠiÅlem "DiÄer" kalemine düÅtü â kalem eÅleme haritası güncellenmemiÅ olabilir.'; 141 } else { 142 unclassifiedBanner.classList.add('d-none'); 143 } 144 145 var k1 = report.controlK1, k2 = report.controlK2, k3 = report.controlK3; 146 $('wlK1Badge').className = badgeClassForStatus(k1.isBreach); 147 $('wlK1Badge').textContent = k1.isBreach ? 'SAPMA' : 'OK'; 148 $('wlK1Msg').textContent = k1.message; 149 150 $('wlK2Badge').className = badgeClassForStatus(k2.isBreach); 151 $('wlK2Badge').textContent = k2.isBreach ? 'SAPMA' : 'OK'; 152 $('wlK2Msg').textContent = k2.message; 153 154 $('wlK3Badge').className = badgeClassForStripe(k3.status); 155 $('wlK3Badge').textContent = k3.status; 156 var k3msg = k3.reason || ''; 157 if (k3.currency) { 158 k3msg = (k3msg ? k3msg + ' â ' : '') + 159 'Payments ' + k3.currency + ': ' + Number((k3.stripeAvailable || 0) + (k3.stripePending || 0)).toFixed(2) + 160 (k3.convertedLiability != null ? (', çevrilmiÅ yükümlülük: ' + Number(k3.convertedLiability).toFixed(2)) : ''); 161 } 162 // Issue #2067 (K3) â Treasury financial account kırılımı (aylık otomatik payout Payments'tan 163 // buraya taÅıyor; para Stripe'tan çıkmıyor). 164 if (k3.treasuryAvailable != null) { 165 k3msg += ' | Treasury ' + (k3.treasuryCurrency || '') + ': ' + Number(k3.treasuryAvailable).toFixed(2) + 166 (k3.treasuryIncludedInTotal ? '' : ' (farklı para birimi â toplama dahil deÄil)'); 167 } 168 if (k3.stripeComparisonTotal != null) { 169 k3msg += ' | Toplam: ' + Number(k3.stripeComparisonTotal).toFixed(2); 170 } 171 $('wlK3Msg').textContent = k3msg; 172 173 var lineBody = $('wlLineItemsBody'); 174 lineBody.innerHTML = ''; 175 (report.lineItems || []).forEach(function (line) { 176 var tr = document.createElement('tr'); 177 if (line.amount > 0) tr.className = 'wl-row-positive'; 178 else if (line.amount < 0) tr.className = 'wl-row-negative'; 179 tr.innerHTML = 180 '<td>' + escapeHtml(line.label) + '</td>' + 181 '<td>' + line.transactionCount + '</td>' + 182 '<td class="wl-amount">' + fmtTry(line.amount) + '</td>'; 183 lineBody.appendChild(tr); 184 }); 185 186 if (report.userBreakdown && report.userBreakdown.length > 0) { 187 renderUsers(report); 188 } 189 } 190 191 function renderUsers(report) { 192 $('wlUserBreakdownCount').textContent = '(' + report.userBreakdownTotalCount + ' kullanıcı)'; 193 var body = $('wlUsersBody'); 194 body.innerHTML = ''; 195 if (!report.userBreakdown || report.userBreakdown.length === 0) { 196 body.innerHTML = '<tr><td colspan="5" class="wl-empty">Kayıt yok.</td></tr>'; 197 return; 198 } 199 report.userBreakdown.forEach(function (u) { 200 var tr = document.createElement('tr'); 201 // T-20260806, Epic #1479, Faz-1 (Karar 3): negatif bakiye = borç â kırmızı satır 202 // (.wl-row-negative, --bd-danger-700 tokeni â mevcut kalem tablosu deseni). 203 if (u.availableBalance < 0) tr.className = 'wl-row-negative'; 204 tr.innerHTML = 205 '<td>' + escapeHtml(u.displayName) + '</td>' + 206 '<td class="wl-amount">' + fmtTry(u.availableBalance) + '</td>' + 207 '<td>' + fmtTry(u.heldBalance) + '</td>' +
208 // Reviewer Minor fix: Emanet sütunu â Toplam = Available+Held+Escrow, sütunsuz 209 // ekranda "tutmuyormuÅ" gibi görünürdü (Faz-1'de her zaman 0). 210 '<td>' + fmtTry(u.escrowBalance) + '</td>' + 211 '<td>' + fmtTry(u.totalBalance) + '</td>'; 212 body.appendChild(tr); 213 }); 214 } 215 216 function statusBadgeHtml(status) { 217 var cls = status === 'Ok' ? 'bd-badge bd-badge--success' : (status === 'Warning' ? 'bd-badge bd-badge--warn' : 'bd-badge bd-badge--danger'); 218 return '<span class="' + cls + '">' + escapeHtml(status) + '</span>'; 219 } 220 221 function renderRuns(runs) { 222 var body = $('wlRunsBody'); 223 body.innerHTML = ''; 224 if (!runs || runs.length === 0) { 225 body.innerHTML = '<tr><td colspan="7" class="wl-empty">Henüz koÅu yok.</td></tr>'; 226 return; 227 } 228 runs.forEach(function (r) { 229 var tr = document.createElement('tr'); 230 tr.innerHTML = 231 '<td>' + fmtDate(r.runUtc) + '</td>' + 232 '<td>' + escapeHtml(r.triggeredBy) + '</td>' + 233 '<td>' + fmtTry(r.ledgerDelta) + '</td>' + 234 '<td>' + fmtTry(r.receiptOverrun) + '</td>' + 235 '<td>' + escapeHtml(r.stripeCheckStatus || '-') + '</td>' + 236 '<td>' + statusBadgeHtml(r.overallStatus) + '</td>' + 237 '<td>' + (r.durationMs != null ? (r.durationMs + ' ms') : '-') + '</td>'; 238 body.appendChild(tr); 239 }); 240 } 241 242 function escapeHtml(s) { 243 if (s == null) return ''; 244 return String(s) 245 .replace(/&/g, '&') 246 .replace(/</g, '<') 247 .replace(/>/g, '>') 248 .replace(/"/g, '"'); 249 } 250 251 function showError(message) { 252 var banner = $('wlErrorBanner'); 253 banner.textContent = message; 254 banner.classList.remove('d-none'); 255 } 256 257 function hideError() { 258 $('wlErrorBanner').classList.add('d-none'); 259 } 260 261 async function loadReport(includeUsers) { 262 $('wlLoadingBanner').classList.remove('d-none'); 263 $('wlContent').classList.add('d-none'); 264 hideError(); 265 try { 266 var report = await apiGet('?includeUsers=' + (includeUsers ? 'true' : 'false') + currentPeriod()); 267 renderReport(report); 268 $('wlLoadingBanner').classList.add('d-none'); 269 $('wlContent').classList.remove('d-none'); 270 } catch (err) { 271 $('wlLoadingBanner').classList.add('d-none'); 272 showError('Rapor yüklenemedi: ' + err.message); 273 } 274 } 275 276 async function loadRuns() { 277 try { 278 var runs = await apiGet('/runs?take=20'); 279 renderRuns(runs); 280 } catch (err) { 281 // Son koÅu listesi hatası ana raporu bloklamaz â sessizce logla. 282 console.error('Wallet liability runs load failed', err); 283 } 284 } 285 286 // Issue #2068 (Reviewer M3) â Reconcile yanıtındaki k3NotEvaluatedStreak alanı yalnız 287 // DevTools > Network'te görünüyordu; admin körlüÄü ekranda görünsün diye eklendi. 288 // textContent kullanılır (innerHTML DEÄİL) â sunucudan gelen sayısal alan olsa da XSS'e 289 // KARÅI güvenli tutuluyor. 290 function showK3StreakWarning(result) { 291 var banner = $('wlK3StreakBanner'); 292 if (!banner) return; 293 var streak = Number(result && result.k3NotEvaluatedStreak || 0); 294 if (streak >= 3) { 295 $('wlK3StreakText').textContent = 296 'K3 (Stripe bakiyesi) art arda ' + streak + ' koÅudur deÄerlendirilemiyor â bu kontrol Åu an kör.'; 297 banner.classList.remove('d-none'); 298 } else { 299 banner.classList.add('d-none'); 300 } 301 } 302 303 async function reconcileNow() { 304 var btn = $('wlReconcileBtn'); 305 btn.disabled = true; 306 var originalHtml = btn.innerHTML; 307 btn.innerHTML = '<i class="bi bi-hourglass-split"></i> ÃalıÅıyor...'; 308 try { 309 var result = await apiPost('/reconcile'); 310 showK3StreakWarning(result); 311 await loadReport(false); 312 await loadRuns(); 313 } catch (err) { 314 showError('Mutabakat koÅusu baÅarısız: ' + err.message); 315 } finally { 316 btn.disabled = false;
317 btn.innerHTML = originalHtml; 318 } 319 } 320 321 function initFilterToggle() { 322 var toggle = document.querySelector('[data-filter-toggle]'); 323 if (!toggle) return; 324 toggle.addEventListener('click', function () { 325 var panelId = toggle.getAttribute('data-filter-toggle'); 326 var panel = $(panelId); 327 if (panel) panel.classList.toggle('d-none'); 328 }); 329 } 330 331 function initExportLinks() { 332 $('wlExportCsvSummary').addEventListener('click', function (e) { 333 e.preventDefault(); 334 window.location.href = apiBase + '/export.csv?section=summary' + currentPeriod(); 335 }); 336 $('wlExportCsvUsers').addEventListener('click', function (e) { 337 e.preventDefault(); 338 window.location.href = apiBase + '/export.csv?section=users' + currentPeriod(); 339 }); 340 $('wlExportPdf').addEventListener('click', function (e) { 341 e.preventDefault(); 342 window.location.href = apiBase + '/export.pdf' + currentPeriod(); 343 }); 344 } 345 346 function init() { 347 initDefaultPeriod(); 348 initFilterToggle(); 349 initExportLinks(); 350 351 $('wlApplyFilterBtn').addEventListener('click', function () { loadReport(false); }); 352 $('wlRefreshBtn').addEventListener('click', function () { loadReport(false); loadRuns(); }); 353 $('wlReconcileBtn').addEventListener('click', reconcileNow); 354 $('wlLoadUsersBtn').addEventListener('click', function () { loadReport(true); }); 355 356 loadReport(false); 357 loadRuns(); 358 } 359 360 document.addEventListener('DOMContentLoaded', init); 361})();
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.