PageSourceSearch

https://binderim.com/js/admin/wallet-accounts.kjt8tdpvb4.js

js binderim.com collected 2026-09-24 14:40:10 UTC 19,176 bytes, 442 lines download raw bytes

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
26            .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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.