nodebb-plugin-niki-loyalty 1.0.22 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/library.js CHANGED
@@ -14,10 +14,7 @@ const SETTINGS = {
14
14
  pointsPerHeartbeat: 5,
15
15
  dailyCap: 250,
16
16
 
17
- // Tek bar hedefi
18
17
  barMax: 250,
19
-
20
- // ✅ 4 kademe (Seçenek B: claim = puan düşer)
21
18
  rewards: [
22
19
  { id: 'cookie', at: 60, title: 'Ücretsiz Kurabiye', type: 'free_item', meta: { item: 'cookie' } },
23
20
  { id: 'c35', at: 120, title: '%35 İndirimli Kahve', type: 'discount', meta: { product: 'coffee', percent: 35 } },
@@ -25,13 +22,10 @@ const SETTINGS = {
25
22
  { id: 'coffee', at: 250, title: 'Ücretsiz Kahve', type: 'free_item', meta: { item: 'coffee' } },
26
23
  ],
27
24
 
28
- // Eski akış uyumluluğu (istersen sonra kaldırırız)
29
- coffeeCost: 250,
30
-
31
- // Kupon token süreleri (sn)
32
25
  couponTTLSeconds: 10 * 60, // 10 dk
33
26
  };
34
27
 
28
+
35
29
  // ✅ TEST: sınırsız kullanım (puan kontrolünü kapatmak için true)
36
30
  const TEST_MODE_UNLIMITED = false;
37
31
 
@@ -226,60 +220,65 @@ Plugin.init = async function (params) {
226
220
  // Client bu endpoint'i çağıracak.
227
221
  // -------------------------
228
222
  router.post('/api/niki-loyalty/claim', middleware.ensureLoggedIn, async (req, res) => {
229
- try {
230
- const uid = req.uid;
231
- const rewardId = (req.body && req.body.rewardId) ? String(req.body.rewardId) : '';
232
- const reward = findRewardById(rewardId);
233
-
234
- if (!reward) {
235
- return res.json({ success: false, message: 'Geçersiz ödül' });
236
- }
237
-
238
- const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
239
-
240
- if (!TEST_MODE_UNLIMITED && points < reward.at) {
241
- return res.json({ success: false, message: 'Yetersiz puan' });
242
- }
243
-
244
- // ✅ Puan düş
245
- if (!TEST_MODE_UNLIMITED) {
246
- await user.decrementUserFieldBy(uid, 'niki_points', reward.at);
247
- }
248
-
249
- const newBalance = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
250
-
251
- // ✅ Kupon oluştur (kasa tarayacak)
252
- const token = makeToken();
253
- const couponKey = `niki:coupon:${token}`;
254
-
255
- const couponPayload = safeStringify({
256
- token,
257
- ts: Date.now(),
258
- rewardId: reward.id,
259
- at: reward.at,
260
- title: reward.title,
261
- type: reward.type,
262
- meta: reward.meta,
263
- ownerUid: uid,
264
- });
265
-
266
- await db.set(couponKey, couponPayload);
267
- await db.expire(couponKey, SETTINGS.couponTTLSeconds);
268
-
269
- // user log
270
- await addUserLog(uid, 'spend', reward.at, `Ödül alındı: ${reward.title}`, { rewardId: reward.id });
223
+ try {
224
+ const uid = req.uid;
225
+ const rewardId = (req.body && req.body.rewardId) ? String(req.body.rewardId) : '';
226
+ const reward = findRewardById(rewardId);
227
+ if (!reward) return res.json({ success: false, message: 'Geçersiz ödül' });
228
+
229
+ const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
230
+ if (!TEST_MODE_UNLIMITED && points < reward.at) {
231
+ return res.json({ success: false, message: 'Yetersiz puan' });
232
+ }
271
233
 
234
+ // ✅ Aynı ödülden aktif kupon varsa tekrar üretme (spam engeli)
235
+ const reserveKey = `niki:reserve:${uid}:${reward.id}`;
236
+ const existingToken = await db.get(reserveKey);
237
+ if (existingToken) {
272
238
  return res.json({
273
239
  success: true,
274
- token,
240
+ token: existingToken,
275
241
  reward: { id: reward.id, at: reward.at, title: reward.title, type: reward.type, meta: reward.meta },
276
- newBalance,
277
- message: 'Ödül hazır! Kasada okutarak kullan.',
242
+ message: 'Zaten aktif bir kuponun var. Kasada okutabilirsin.',
278
243
  });
279
- } catch (err) {
280
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
281
244
  }
282
- });
245
+
246
+ // ✅ token oluştur
247
+ const token = makeToken();
248
+ const couponKey = `niki:coupon:${token}`;
249
+
250
+ const couponPayload = safeStringify({
251
+ token,
252
+ ts: Date.now(),
253
+ rewardId: reward.id,
254
+ cost: reward.at, // ✅ puan düşülecek tutar
255
+ title: reward.title,
256
+ type: reward.type,
257
+ meta: reward.meta,
258
+ ownerUid: uid,
259
+ });
260
+
261
+ await db.set(couponKey, couponPayload);
262
+ await db.expire(couponKey, SETTINGS.couponTTLSeconds);
263
+
264
+ // ✅ rezervasyonu da tut
265
+ await db.set(reserveKey, token);
266
+ await db.expire(reserveKey, SETTINGS.couponTTLSeconds);
267
+
268
+ // user log (claim logu - puan düşmeden)
269
+ await addUserLog(uid, 'spend', 0, `Kupon oluşturuldu: ${reward.title}`, { rewardId: reward.id, token });
270
+
271
+ return res.json({
272
+ success: true,
273
+ token,
274
+ reward: { id: reward.id, at: reward.at, title: reward.title, type: reward.type, meta: reward.meta },
275
+ message: 'Kupon hazır! Kasada okutunca puanın düşer.',
276
+ });
277
+ } catch (err) {
278
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
279
+ }
280
+ });
281
+
283
282
 
284
283
  // -------------------------
285
284
  // 4) KASA HISTORY (admin/mod)
@@ -341,54 +340,65 @@ Plugin.init = async function (params) {
341
340
  // 5) KASA: COUPON SCAN (admin/mod) ✅ yeni akış
342
341
  // Claim ile üretilen token kasada okutulur.
343
342
  // -------------------------
344
- router.post('/api/niki-loyalty/scan-coupon', middleware.ensureLoggedIn, async (req, res) => {
345
- try {
346
- const token = (req.body && req.body.token) ? String(req.body.token) : '';
347
-
348
- const staffOk = await isStaff(req.uid);
349
- if (!staffOk) return res.status(403).json({ success: false, message: 'Yetkisiz' });
343
+ router.post('/api/niki-loyalty/scan-coupon', middleware.ensureLoggedIn, async (req, res) => {
344
+ try {
345
+ const token = (req.body && req.body.token) ? String(req.body.token) : '';
350
346
 
351
- const raw = await db.get(`niki:coupon:${token}`);
352
- if (!raw) return res.json({ success: false, message: 'Geçersiz / Süresi dolmuş kupon' });
347
+ const staffOk = await isStaff(req.uid);
348
+ if (!staffOk) return res.status(403).json({ success: false, message: 'Yetkisiz' });
353
349
 
354
- const coupon = safeParseMaybeJson(raw);
355
- if (!coupon || !coupon.ownerUid) {
356
- await db.delete(`niki:coupon:${token}`);
357
- return res.json({ success: false, message: 'Kupon bozuk' });
358
- }
350
+ const raw = await db.get(`niki:coupon:${token}`);
351
+ if (!raw) return res.json({ success: false, message: 'Geçersiz / Süresi dolmuş kupon' });
359
352
 
360
- // tek kullanımlık
353
+ const coupon = safeParseMaybeJson(raw);
354
+ if (!coupon || !coupon.ownerUid || !coupon.rewardId) {
361
355
  await db.delete(`niki:coupon:${token}`);
356
+ return res.json({ success: false, message: 'Kupon bozuk' });
357
+ }
362
358
 
363
- const customerUid = parseInt(coupon.ownerUid, 10);
364
- const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
359
+ const customerUid = parseInt(coupon.ownerUid, 10);
360
+ const cost = parseInt(coupon.cost || 0, 10);
365
361
 
366
- // kasa log (artık amount = coupon.at)
367
- await addKasaLog(
368
- req.uid,
369
- customerData?.username || 'Bilinmeyen',
370
- customerUid,
371
- parseInt(coupon.at || 0, 10),
372
- coupon.rewardId,
373
- coupon.title
374
- );
362
+ // puan yeter mi? (scan anında kontrol)
363
+ const pts = parseInt((await user.getUserField(customerUid, 'niki_points')) || 0, 10);
364
+ if (!TEST_MODE_UNLIMITED && pts < cost) {
365
+ return res.json({ success: false, message: 'Yetersiz bakiye (puan değişmiş olabilir)' });
366
+ }
375
367
 
376
- return res.json({
377
- success: true,
378
- customer: customerData,
379
- coupon: {
380
- rewardId: coupon.rewardId,
381
- title: coupon.title,
382
- type: coupon.type,
383
- meta: coupon.meta,
384
- at: coupon.at,
385
- },
386
- message: 'Kupon onaylandı!',
387
- });
388
- } catch (err) {
389
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
368
+ // ✅ puan düş (asıl düşüş burada)
369
+ if (!TEST_MODE_UNLIMITED && cost > 0) {
370
+ await user.decrementUserFieldBy(customerUid, 'niki_points', cost);
390
371
  }
391
- });
372
+
373
+ // ✅ tek kullanımlık: kuponu sil
374
+ await db.delete(`niki:coupon:${token}`);
375
+ // ✅ rezervasyonu da sil
376
+ await db.delete(`niki:reserve:${customerUid}:${coupon.rewardId}`);
377
+
378
+ const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
379
+
380
+ // user log (gerçek spend burada)
381
+ await addUserLog(customerUid, 'spend', cost, `Kullanıldı: ${coupon.title}`, { rewardId: coupon.rewardId });
382
+
383
+ // kasa log
384
+ await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid, cost, coupon.rewardId, coupon.title);
385
+
386
+ return res.json({
387
+ success: true,
388
+ customer: customerData,
389
+ coupon: {
390
+ rewardId: coupon.rewardId,
391
+ title: coupon.title,
392
+ type: coupon.type,
393
+ meta: coupon.meta,
394
+ cost,
395
+ },
396
+ message: 'Kupon onaylandı!',
397
+ });
398
+ } catch (err) {
399
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
400
+ }
401
+ });
392
402
 
393
403
  // -------------------------
394
404
  // 6) (ESKİ) QR OLUŞTUR / OKUT — opsiyonel uyumluluk
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-niki-loyalty",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "description": "Niki The Cat Coffee Loyalty System - Earn points while studying on IEU Forum.",
5
5
  "main": "library.js",
6
6
  "nbbpm": {
@@ -1,10 +1,27 @@
1
1
  'use strict';
2
2
 
3
3
  $(document).ready(function () {
4
- // --- AYARLAR ---
4
+ // =========================================
5
+ // AYARLAR
6
+ // =========================================
5
7
  const NIKI_LOGO_URL = "https://i.ibb.co/nZvtpss/logo-placeholder.png";
6
8
 
7
- // Widget HTML Şablonu
9
+ // 60/120/180/250 QR üret (kasa okutup puan düşsün) ✅ senin istediğin
10
+ // Not: Backend tarafında /api/niki-loyalty/claim rewardId ile token döndürmeli,
11
+ // puan düşümü scan-coupon tarafında olmalı.
12
+ const REWARDS_FALLBACK = [
13
+ { id: 'cookie', at: 60, title: 'Ücretsiz Kurabiye' },
14
+ { id: 'c35', at: 120, title: '%35 İndirimli Kahve' },
15
+ { id: 'c60', at: 180, title: '%60 İndirimli Kahve' },
16
+ { id: 'coffee', at: 250, title: 'Ücretsiz Kahve' },
17
+ ];
18
+
19
+ // QR ekranda kalma süresi (eski gibi) ✅ 2 dakika
20
+ const QR_TTL_SECONDS = 120;
21
+
22
+ // =========================================
23
+ // FLOATING WIDGET
24
+ // =========================================
8
25
  const widgetHtml = `
9
26
  <div id="niki-floating-widget" class="niki-hidden">
10
27
  <div class="niki-widget-content" onclick="ajaxify.go('niki-wallet')">
@@ -17,16 +34,12 @@ $(document).ready(function () {
17
34
  </div>
18
35
  `;
19
36
 
20
- // -------------------------
21
- // UI Helpers
22
- // -------------------------
23
37
  function fixLogo() {
24
38
  const img = document.querySelector("img.niki-widget-logo");
25
39
  if (img && img.src !== NIKI_LOGO_URL) img.src = NIKI_LOGO_URL;
26
40
  }
27
41
 
28
42
  function showNikiToast(msg, kind) {
29
- // kind: 'ok' | 'warn' | 'err' (şimdilik css yoksa da sorun değil)
30
43
  $('.niki-toast').remove();
31
44
  const icon = (kind === 'err') ? 'fa-triangle-exclamation' : (kind === 'warn' ? 'fa-circle-info' : 'fa-paw');
32
45
  const toast = $(`<div class="niki-toast niki-${kind || 'ok'}"><i class="fa ${icon}"></i> ${msg}</div>`);
@@ -44,184 +57,18 @@ $(document).ready(function () {
44
57
  $('#niki-floating-widget').removeClass('niki-hidden');
45
58
  localStorage.setItem('niki_last_points', String(p));
46
59
 
47
- // Wallet sayfasındaysan, oradaki puan UI'ını da güncellemeyi dene (id/class varsa)
48
- // (Şablon değişken olabilir, yoksa dokunmaz)
49
- $('#niki-wallet-points, .niki-wallet-points').text(p);
60
+ // Wallet tpl içinde varsa güncelle
61
+ $('#my-points, #niki-wallet-points, .niki-wallet-points').text(p);
62
+ $('#target-txt').text(`${p} / 250`);
50
63
  }
51
64
 
52
- // -------------------------
53
- // Data Fetch (wallet-data)
54
- // -------------------------
55
- function fetchWalletData(opts) {
56
- opts = opts || {};
57
- return $.get('/api/niki-loyalty/wallet-data')
58
- .done(function (data) {
59
- if (!data) return;
60
- setPointsUI(data.points || 0);
61
-
62
- // İstersen wallet sayfasında bar/milestone UI render etmek için data’yı sakla
63
- window.__NIKI_WALLET_DATA__ = data;
64
-
65
- // Wallet template'in içine "rewards" render eden bir alan koyduysan burada çağır
66
- // (Alan yoksa hiçbir şey olmaz, güvenli)
67
- if (opts.renderRewards) {
68
- renderWalletRewards(data);
69
- }
70
- })
71
- .fail(function () {
72
- // cache yoksa 0 göster
73
- const cached = localStorage.getItem('niki_last_points');
74
- if (cached === null) {
75
- setPointsUI(0);
76
- }
77
- });
78
- }
79
-
80
- // -------------------------
81
- // Wallet Rewards Render + Claim (Seçenek B)
82
- // Bu render ancak sayfanda #niki-rewards gibi bir container varsa çalışır.
83
- // Yoksa dokunmaz.
84
- // -------------------------
85
- function renderWalletRewards(data) {
86
- const $wrap = $('#niki-rewards');
87
- if ($wrap.length === 0) return; // container yoksa geç
88
-
89
- const points = Number(data.points || 0);
90
- const barMax = Number(data.barMax || 250);
91
- const rewards = Array.isArray(data.rewards) ? data.rewards : [];
92
-
93
- // Basit milestone bar (container varsa)
94
- const barPercent = Math.min(100, (points / barMax) * 100);
95
-
96
- let barHtml = `
97
- <div class="niki-mbar">
98
- <div class="niki-mbar-top">
99
- <div class="niki-mbar-title">Ödül Barı</div>
100
- <div class="niki-mbar-val">${points} / ${barMax}</div>
101
- </div>
102
- <div class="niki-mbar-track">
103
- <div class="niki-mbar-fill" style="width:${barPercent}%"></div>
104
- ${rewards.map(r => {
105
- const left = Math.min(100, (Number(r.at) / barMax) * 100);
106
- const unlocked = points >= Number(r.at);
107
- return `<span class="niki-mbar-dot ${unlocked ? 'on' : ''}" style="left:${left}%"
108
- title="${escapeHtml(r.title)} • ${r.at} puan"></span>`;
109
- }).join('')}
110
- </div>
111
- <div class="niki-mbar-hint">Ödülü aldığında puanın düşer.</div>
112
- </div>
113
- `;
114
-
115
- let cardsHtml = rewards.map(r => {
116
- const at = Number(r.at || 0);
117
- const unlocked = points >= at;
118
-
119
- const badge = unlocked ? `<span class="niki-r-badge on">Alınabilir</span>` : `<span class="niki-r-badge">Kilitli</span>`;
120
- const btn = unlocked
121
- ? `<button class="btn btn-sm btn-primary niki-claim-btn" data-reward="${escapeAttr(r.id)}">
122
- Ödülü Al
123
- </button>`
124
- : `<button class="btn btn-sm btn-default niki-claim-btn" disabled>
125
- ${at} puan gerekli
126
- </button>`;
127
-
128
- return `
129
- <div class="niki-r-card ${unlocked ? 'on' : ''}">
130
- <div class="niki-r-left">
131
- <div class="niki-r-title">${escapeHtml(r.title)}</div>
132
- <div class="niki-r-sub">${at} puan</div>
133
- </div>
134
- <div class="niki-r-right">
135
- ${badge}
136
- ${btn}
137
- </div>
138
- </div>
139
- `;
140
- }).join('');
141
-
142
- $wrap.html(barHtml + `<div class="niki-r-list">${cardsHtml}</div>`);
143
-
144
- // Claim handler
145
- $wrap.off('click.nikiClaim').on('click.nikiClaim', '.niki-claim-btn:not([disabled])', function () {
146
- const rewardId = $(this).data('reward');
147
- if (!rewardId) return;
148
-
149
- // Butonu anlık kilitle (double click engeli)
150
- const $btn = $(this);
151
- $btn.prop('disabled', true).text('Hazırlanıyor...');
152
-
153
- claimReward(rewardId)
154
- .always(() => {
155
- // UI refresh, button state render sonrası zaten güncellenecek
156
- });
157
- });
158
- }
159
-
160
- function claimReward(rewardId) {
161
- return $.ajax({
162
- url: '/api/niki-loyalty/claim',
163
- method: 'POST',
164
- data: {
165
- rewardId: rewardId,
166
- _csrf: config.csrf_token,
167
- },
168
- }).done(function (res) {
169
- if (!res || !res.success) {
170
- showNikiToast(res?.message || 'Ödül alınamadı', 'err');
171
- // Wallet UI’ı tazele
172
- fetchWalletData({ renderRewards: true });
173
- return;
174
- }
175
-
176
- // ✅ Puan düşmüş yeni bakiye
177
- if (typeof res.newBalance !== 'undefined') {
178
- setPointsUI(res.newBalance);
179
- }
180
-
181
- // ✅ Token geldi (kasada okutulacak)
182
- // Token'ı localStorage'a kaydedelim (wallet ekranı QR üretirken kullanabilsin)
183
- if (res.token) {
184
- localStorage.setItem('niki_last_coupon_token', res.token);
185
- }
186
-
187
- showNikiToast(res.message || 'Ödül hazır! Kasada okut.', 'ok');
188
-
189
- // Wallet sayfasını tazele ve rewards’ları yeniden render et
190
- fetchWalletData({ renderRewards: true });
191
-
192
- // Eğer wallet template’in içinde "QR göster" alanın varsa, event ile haber ver
193
- $(window).trigger('niki:coupon.ready', [res]);
194
- }).fail(function () {
195
- showNikiToast('Sunucu hatası: ödül alınamadı', 'err');
196
- fetchWalletData({ renderRewards: true });
197
- });
198
- }
199
-
200
- // Basit escape helper’lar (XSS koruması)
201
- function escapeHtml(str) {
202
- return String(str || '')
203
- .replaceAll('&', '&amp;')
204
- .replaceAll('<', '&lt;')
205
- .replaceAll('>', '&gt;')
206
- .replaceAll('"', '&quot;')
207
- .replaceAll("'", '&#39;');
208
- }
209
- function escapeAttr(str) {
210
- return escapeHtml(str).replaceAll(' ', '');
211
- }
212
-
213
- // -------------------------
214
- // 1) Widget Başlatma ve Veri Yönetimi
215
- // -------------------------
216
65
  function initNikiWidget() {
217
66
  if (!app.user.uid || app.user.uid <= 0) return;
218
67
 
219
- // Widget yoksa ekle
220
68
  if ($('#niki-floating-widget').length === 0) {
221
69
  $('body').append(widgetHtml);
222
70
  }
223
71
 
224
- // Cache: anında göster
225
72
  const cachedPoints = localStorage.getItem('niki_last_points');
226
73
  if (cachedPoints !== null) {
227
74
  $('#niki-live-points').text(cachedPoints);
@@ -230,25 +77,38 @@ $(document).ready(function () {
230
77
 
231
78
  fixLogo();
232
79
 
233
- // Sunucudan taze veri çek
234
- const onWalletPage = (ajaxify.data && ajaxify.data.template === 'niki-wallet');
235
- fetchWalletData({ renderRewards: onWalletPage }).always(function () {
80
+ $.get('/api/niki-loyalty/wallet-data', function (data) {
81
+ const freshPoints = data?.points || 0;
82
+ setPointsUI(freshPoints);
236
83
  fixLogo();
84
+
85
+ // Wallet sayfasındaysa rewards render et
86
+ if (isWalletPage()) {
87
+ renderWalletFromData(data);
88
+ }
89
+ }).fail(function () {
90
+ if (cachedPoints === null) {
91
+ setPointsUI(0);
92
+ }
237
93
  });
238
94
  }
239
95
 
240
- // Başlat
96
+ function isWalletPage() {
97
+ // nodebb template adı her projede değişebilir; pathname ile de kontrol edelim
98
+ const p = window.location.pathname || '';
99
+ return (ajaxify?.data?.template === 'niki-wallet') || p.indexOf('niki-wallet') > -1;
100
+ }
101
+
241
102
  initNikiWidget();
242
103
 
243
- // Sayfa geçişlerinde tekrar çalıştır
244
104
  $(window).on('action:ajaxify.end', function () {
245
105
  initNikiWidget();
246
106
  setTimeout(fixLogo, 400);
247
107
  });
248
108
 
249
- // -------------------------
250
- // --- AKTİFLİK SİSTEMİ (Heartbeat) ---
251
- // -------------------------
109
+ // =========================================
110
+ // HEARTBEAT
111
+ // =========================================
252
112
  let activeSeconds = 0;
253
113
  let isUserActive = false;
254
114
  let idleTimer;
@@ -261,7 +121,7 @@ $(document).ready(function () {
261
121
  $(window).on('mousemove scroll keydown click touchstart', resetIdleTimer);
262
122
 
263
123
  setInterval(() => {
264
- if (ajaxify.data && ajaxify.data.template && ajaxify.data.template.topic && document.visibilityState === 'visible' && isUserActive) {
124
+ if (ajaxify?.data?.template?.topic && document.visibilityState === 'visible' && isUserActive) {
265
125
  activeSeconds++;
266
126
  }
267
127
  if (activeSeconds >= 60) {
@@ -274,19 +134,318 @@ $(document).ready(function () {
274
134
  $.post('/api/niki-loyalty/heartbeat', { _csrf: config.csrf_token }, function (res) {
275
135
  if (res && res.earned) {
276
136
  setPointsUI(res.total);
277
-
278
137
  showNikiToast(`+${res.points} Puan Kazandın! ⚡`, 'ok');
279
138
 
280
139
  $('#niki-floating-widget').addClass('niki-bounce');
281
140
  setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
282
141
 
283
- // Wallet sayfasındaysan rewards'ları da tazele
284
- const onWalletPage = (ajaxify.data && ajaxify.data.template === 'niki-wallet');
285
- if (onWalletPage) {
286
- fetchWalletData({ renderRewards: true });
142
+ // wallet açıksa güncelle
143
+ if (isWalletPage()) {
144
+ refreshWallet();
287
145
  }
288
146
  }
289
147
  });
290
148
  }
149
+
150
+ // =========================================
151
+ // WALLET: Rewards render + QR flow (2 dk)
152
+ // =========================================
153
+ let pollInterval = null;
154
+ let initialPointsForQR = null;
155
+ let timerInterval = null;
156
+
157
+ function refreshWallet() {
158
+ $.get('/api/niki-loyalty/wallet-data', function (data) {
159
+ setPointsUI(data?.points || 0);
160
+ renderWalletFromData(data);
161
+ });
162
+ }
163
+
164
+ function renderWalletFromData(data) {
165
+ // 1) Bar (varsa)
166
+ try {
167
+ const p = parseInt(data?.points || 0, 10) || 0;
168
+ const goal = parseInt(data?.barMax || 250, 10) || 250;
169
+ const pct = Math.min(100, (p / goal) * 100);
170
+ $('#prog-bar').css('width', pct + '%');
171
+ $('#target-txt').text(`${p} / ${goal}`);
172
+ } catch (e) {}
173
+
174
+ // 2) Rewards container varsa bas
175
+ const $wrap = $('#niki-rewards');
176
+ if ($wrap.length === 0) return;
177
+
178
+ const points = parseInt(data?.points || 0, 10) || 0;
179
+ const rewards = Array.isArray(data?.rewards) && data.rewards.length ? data.rewards : REWARDS_FALLBACK.map(r => ({
180
+ id: r.id, at: r.at, title: r.title, unlocked: points >= r.at
181
+ }));
182
+
183
+ const cards = rewards.map(r => {
184
+ const at = parseInt(r.at || 0, 10) || 0;
185
+ const unlocked = points >= at;
186
+
187
+ const btnText = unlocked ? 'QR OLUŞTUR' : `${at - points} PUAN EKSİK`;
188
+ const btnDisabled = unlocked ? '' : 'disabled';
189
+
190
+ return `
191
+ <div class="niki-r-card ${unlocked ? 'on' : ''}" style="display:flex;align-items:center;justify-content:space-between;padding:12px 12px;border-radius:16px;background:#FAFAFA;border:1px solid #EEE;margin:10px 0;">
192
+ <div style="text-align:left;">
193
+ <div style="font-weight:800;color:#1a1a1a;font-size:13px;">${escapeHtml(r.title)}</div>
194
+ <div style="color:#888;font-size:11px;font-weight:700;margin-top:2px;">${at} puan</div>
195
+ </div>
196
+ <button class="niki-genqr-btn" data-reward="${escapeAttr(r.id)}" data-title="${escapeAttr(r.title)}" data-cost="${at}"
197
+ ${btnDisabled}
198
+ style="border:none;border-radius:14px;padding:12px 12px;font-weight:900;font-size:12px;cursor:${unlocked ? 'pointer' : 'not-allowed'};background:${unlocked ? '#1a1a1a' : '#E0E0E0'};color:${unlocked ? '#fff' : '#999'};min-width:120px;">
199
+ ${btnText}
200
+ </button>
201
+ </div>
202
+ `;
203
+ }).join('');
204
+
205
+ $wrap.html(`
206
+ <div style="font-size:11px;color:#888;text-align:left;margin:6px 0 10px;font-weight:700;">
207
+ QR oluşturunca puanın <b>kasada okutulunca</b> düşer. (2 dakika geçerli)
208
+ </div>
209
+ ${cards}
210
+ `);
211
+
212
+ // click bind
213
+ $wrap.off('click.nikiGen').on('click.nikiGen', '.niki-genqr-btn:not([disabled])', function () {
214
+ const rewardId = $(this).data('reward');
215
+ const title = $(this).data('title') || 'Ödül';
216
+ const cost = parseInt($(this).data('cost') || 0, 10) || 0;
217
+
218
+ generateRewardQR({ rewardId, title, cost });
219
+ });
220
+ }
221
+
222
+ function generateRewardQR({ rewardId, title, cost }) {
223
+ // qr modal varsa onu kullan, yoksa basit popup yap
224
+ if (!rewardId) return;
225
+
226
+ // güvenli: o anda puan snapshot
227
+ $.get('/api/niki-loyalty/wallet-data', function (data) {
228
+ const points = parseInt(data?.points || 0, 10) || 0;
229
+ if (points < cost) {
230
+ showNikiToast('Yetersiz puan', 'warn');
231
+ refreshWallet();
232
+ return;
233
+ }
234
+
235
+ if (!confirm(`${title} için QR oluşturulsun mu? (2 dakika geçerli)`)) return;
236
+
237
+ // ✅ Claim = sadece token üret (puan düşmez), okutunca düşer
238
+ $.post('/api/niki-loyalty/claim', { rewardId, _csrf: config.csrf_token }, function (res) {
239
+ if (!res || !res.success || !res.token) {
240
+ showNikiToast(res?.message || 'QR oluşturulamadı', 'err');
241
+ return;
242
+ }
243
+
244
+ initialPointsForQR = points;
245
+ openQRModal({
246
+ token: res.token,
247
+ title: title,
248
+ ttlSeconds: QR_TTL_SECONDS,
249
+ });
250
+
251
+ startPollingForSuccess();
252
+ }).fail(function () {
253
+ showNikiToast('Sunucu hatası (claim)', 'err');
254
+ });
255
+ });
256
+ }
257
+
258
+ function openQRModal({ token, title, ttlSeconds }) {
259
+ // Wallet tpl'nin modalını destekle
260
+ const $modal = $('#modal-qr');
261
+ const hasTplModal = $modal.length > 0;
262
+
263
+ if (!hasTplModal) {
264
+ // fallback: hızlı modal enjekte
265
+ injectFallbackQRModal();
266
+ }
267
+
268
+ // tpl modal elemanları varsa set
269
+ $('#view-code').show();
270
+ $('#view-success').hide();
271
+ $('#ticket-title').text((title || 'ÖDÜL') + ' • KASAYA GÖSTER');
272
+ $('#ticket-sub').text('QR’ı kasada okut.');
273
+
274
+ // QR bas (qrcodejs yoksa token yaz)
275
+ const qrEl = document.getElementById('qrcode');
276
+ if (qrEl) {
277
+ $(qrEl).empty();
278
+
279
+ if (typeof QRCode !== 'undefined') {
280
+ // Büyük, okunaklı QR
281
+ new QRCode(qrEl, {
282
+ text: String(token),
283
+ width: 260,
284
+ height: 260,
285
+ colorDark: "#000000",
286
+ colorLight: "#ffffff",
287
+ correctLevel: QRCode.CorrectLevel.L
288
+ });
289
+ } else {
290
+ $(qrEl).html(`<div style="font-weight:900;word-break:break-all;">${escapeHtml(token)}</div>`);
291
+ }
292
+ }
293
+
294
+ // modal aç
295
+ $('#modal-qr').fadeIn(200).css('display', 'flex');
296
+
297
+ // timer
298
+ startTimer(ttlSeconds);
299
+ }
300
+
301
+ function startTimer(durationSec) {
302
+ clearInterval(timerInterval);
303
+ let timer = durationSec;
304
+ const total = durationSec;
305
+
306
+ $('#time-bar').css('width', '100%');
307
+ $('#timer-txt').text(formatTime(timer));
308
+
309
+ timerInterval = setInterval(function () {
310
+ // modal kapanırsa dur
311
+ if (!$('#modal-qr').is(':visible') || $('#view-success').is(':visible')) {
312
+ clearInterval(timerInterval);
313
+ timerInterval = null;
314
+ return;
315
+ }
316
+
317
+ const pct = Math.max(0, (timer / total) * 100);
318
+ $('#time-bar').css('width', pct + '%');
319
+ $('#timer-txt').text(formatTime(timer));
320
+
321
+ timer -= 1;
322
+ if (timer < 0) {
323
+ clearInterval(timerInterval);
324
+ timerInterval = null;
325
+ closeQRModal();
326
+ showNikiToast('Süre doldu, kod geçersiz olabilir.', 'warn');
327
+ }
328
+ }, 1000);
329
+ }
330
+
331
+ function startPollingForSuccess() {
332
+ clearInterval(pollInterval);
333
+
334
+ pollInterval = setInterval(() => {
335
+ // modal açık değilse polling durdur
336
+ if (!$('#modal-qr').is(':visible')) {
337
+ clearInterval(pollInterval);
338
+ pollInterval = null;
339
+ return;
340
+ }
341
+
342
+ // ✅ Eski gibi: puan düşerse -> kasa okutmuş demektir
343
+ $.get('/api/niki-loyalty/wallet-data', function (data) {
344
+ const currentP = parseInt(data?.points || 0, 10) || 0;
345
+
346
+ // Puan düşmüşse başarı
347
+ if (initialPointsForQR != null && currentP < initialPointsForQR) {
348
+ clearInterval(pollInterval);
349
+ pollInterval = null;
350
+
351
+ showSuccessView();
352
+ setPointsUI(currentP);
353
+ // reward list refresh
354
+ renderWalletFromData(data);
355
+ }
356
+ });
357
+ }, 2000);
358
+ }
359
+
360
+ function showSuccessView() {
361
+ $('#view-code').hide();
362
+ $('#view-success').fadeIn(200);
363
+
364
+ // confetti varsa patlat
365
+ try {
366
+ if (typeof confetti !== 'undefined') {
367
+ confetti({ particleCount: 140, spread: 70, origin: { y: 0.6 } });
368
+ }
369
+ } catch (e) {}
370
+ }
371
+
372
+ // tpl'nin closeQR fonksiyonu varsa onu çağır, yoksa kendin kapat
373
+ window.closeQR = window.closeQR || function () { closeQRModal(); };
374
+
375
+ function closeQRModal() {
376
+ $('#modal-qr').fadeOut(200);
377
+ clearInterval(pollInterval);
378
+ pollInterval = null;
379
+ clearInterval(timerInterval);
380
+ timerInterval = null;
381
+ initialPointsForQR = null;
382
+
383
+ // wallet refresh
384
+ if (isWalletPage()) {
385
+ setTimeout(refreshWallet, 400);
386
+ }
387
+ }
388
+
389
+ function injectFallbackQRModal() {
390
+ if ($('#modal-qr').length) return;
391
+
392
+ $('body').append(`
393
+ <div id="modal-qr" class="qr-overlay" style="position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(10,10,10,.92);z-index:10000;">
394
+ <div class="ticket-card" style="background:#fff;width:320px;border-radius:26px;overflow:hidden;position:relative;">
395
+ <div class="close-circle" onclick="closeQR()" style="position:absolute;top:12px;right:12px;width:30px;height:30px;border-radius:50%;background:rgba(0,0,0,.08);display:flex;align-items:center;justify-content:center;cursor:pointer;">
396
+ <i class="fa fa-times"></i>
397
+ </div>
398
+
399
+ <div id="view-code">
400
+ <div class="ticket-top" id="ticket-title" style="background:#111;padding:22px;color:#C5A065;font-weight:900;letter-spacing:2px;font-size:12px;text-align:center;">
401
+ KASAYA GÖSTERİNİZ
402
+ </div>
403
+ <div class="ticket-body" style="padding:30px 22px;text-align:center;">
404
+ <div id="qrcode" style="display:flex;justify-content:center;margin-bottom:16px;"></div>
405
+ <div id="ticket-sub" style="font-size:13px;font-weight:900;color:#111;margin-top:4px;">QR hazır</div>
406
+ <div class="timer-wrapper" style="width:100%;height:6px;background:#eee;border-radius:10px;overflow:hidden;margin-top:12px;">
407
+ <div class="timer-bar" id="time-bar" style="height:100%;background:#C5A065;width:100%;"></div>
408
+ </div>
409
+ <div class="timer-text" id="timer-txt" style="font-size:12px;color:#777;margin-top:8px;font-weight:800;">2:00</div>
410
+ </div>
411
+ </div>
412
+
413
+ <div id="view-success" class="success-view" style="display:none;padding:34px 18px;text-align:center;">
414
+ <div class="success-icon" style="width:92px;height:92px;margin:0 auto 16px;border-radius:50%;background:#2E7D32;color:#fff;display:flex;align-items:center;justify-content:center;font-size:44px;">
415
+ <i class="fa fa-check"></i>
416
+ </div>
417
+ <div style="font-size:20px;font-weight:900;color:#111;">AFİYET OLSUN!</div>
418
+ <div style="font-size:13px;color:#777;margin-top:6px;">Kasa onayı alındı.</div>
419
+ <button class="niki-btn" onclick="closeQR()" style="margin-top:16px;background:#111;color:#fff;width:100%;padding:14px;border-radius:16px;border:none;font-weight:900;">
420
+ TAMAM
421
+ </button>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ `);
426
+ }
427
+
428
+ function formatTime(sec) {
429
+ sec = Math.max(0, parseInt(sec, 10) || 0);
430
+ const m = Math.floor(sec / 60);
431
+ const s = sec % 60;
432
+ return `${m}:${s < 10 ? '0' + s : s}`;
433
+ }
434
+
435
+ function escapeHtml(str) {
436
+ return String(str || '')
437
+ .replaceAll('&', '&amp;')
438
+ .replaceAll('<', '&lt;')
439
+ .replaceAll('>', '&gt;')
440
+ .replaceAll('"', '&quot;')
441
+ .replaceAll("'", '&#39;');
442
+ }
443
+ function escapeAttr(str) {
444
+ return escapeHtml(str).replaceAll(' ', '');
445
+ }
446
+
447
+ // Wallet sayfasına ilk girişte render
448
+ if (isWalletPage()) {
449
+ refreshWallet();
450
+ }
291
451
  });
292
-
@@ -1,3 +1,6 @@
1
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
2
+ <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
3
+
1
4
  <div class="niki-wallet-wrapper">
2
5
  <div class="niki-header-bg"></div>
3
6
 
@@ -7,10 +10,9 @@
7
10
  </div>
8
11
 
9
12
  <div class="niki-balance-label">Toplam Bakiye</div>
10
- <!-- client.js anlık güncelleyebilsin diye id ekledim -->
11
13
  <div class="niki-balance-big" id="niki-wallet-points">{points}</div>
12
14
 
13
- <!-- ✅ ÖDÜL BARI + KADEME DOTLARI (60/120/180/250) -->
15
+ <!-- ✅ ÖDÜL BARI -->
14
16
  <div class="niki-reward-stats">
15
17
  <div style="display:flex; justify-content:space-between; font-size:12px; color:#bdbdbd; font-weight:700;">
16
18
  <span>Ödül Barı</span>
@@ -20,7 +22,6 @@
20
22
  <div class="niki-progress-track niki-reward-track" style="position:relative;">
21
23
  <div class="niki-progress-fill" id="niki-reward-fill" style="width: 0%;"></div>
22
24
 
23
- <!-- Milestone dotlar -->
24
25
  <span class="niki-ms-dot" data-at="60" style="left:24%;" title="60 • Ücretsiz Kurabiye"></span>
25
26
  <span class="niki-ms-dot" data-at="120" style="left:48%;" title="120 • %35 İndirimli Kahve"></span>
26
27
  <span class="niki-ms-dot" data-at="180" style="left:72%;" title="180 • %60 İndirimli Kahve"></span>
@@ -28,37 +29,13 @@
28
29
  </div>
29
30
 
30
31
  <div style="font-size:11px; color:#a7a7a7; margin-top:6px;">
31
- Ödülü aldığında <b style="color:#fff;">puan düşer</b>. (Kasa için QR/kupon oluşur.)
32
+ QR oluşturduktan sonra <b style="color:#fff;">kasada okutulunca</b> puanın düşer. (QR 2 dakika geçerli)
32
33
  </div>
33
34
  </div>
34
35
 
35
- <!-- ✅ REWARD KARTLARI (client.js #niki-rewards içine basacak) -->
36
+ <!-- ✅ REWARD KARTLARI (client.js buraya basacak: QR OLUŞTUR butonları) -->
36
37
  <div id="niki-rewards" style="margin-top:16px;"></div>
37
38
 
38
- <!-- ✅ Kupon/QR alanı (claim sonrası token burada gösterilecek) -->
39
- <div id="niki-coupon-area" style="display:none; margin-top:16px;">
40
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
41
- <div style="font-weight:800; color:#fff;">Kasada Okut</div>
42
- <button class="niki-mini-btn" id="niki-coupon-close" type="button">Kapat</button>
43
- </div>
44
-
45
- <div class="niki-coupon-card">
46
- <div style="font-size:12px; color:#cfcfcf; margin-bottom:8px;">
47
- <span id="niki-coupon-title">Ödül Kuponu</span>
48
- </div>
49
-
50
- <div id="niki-coupon-qr" class="niki-qr-box">
51
- <!-- QR burada üretilecek (client.js event ile doldurabilir) -->
52
- <div style="color:#aaa; font-size:12px;">QR hazırlanıyor...</div>
53
- </div>
54
-
55
- <div style="margin-top:10px; font-size:11px; color:#a7a7a7;">
56
- Kuponun süresi sınırlı olabilir. Kasada okutunca tek kullanımlık olur.
57
- </div>
58
- </div>
59
- </div>
60
-
61
- <!-- ✅ Günlük kazanım aynı kaldı -->
62
39
  <div class="niki-daily-stats" style="margin-top:18px;">
63
40
  <div style="display:flex; justify-content:space-between; font-size:12px; color:#888; font-weight:700;">
64
41
  <span>Günlük Kazanım</span>
@@ -74,21 +51,50 @@
74
51
  </div>
75
52
  </div>
76
53
 
77
- <!-- ❌ Eski kahve QR butonunu kaldırmadım ama UX için “Eski Sistem” diye ayırdım -->
78
- <button class="niki-btn-action" id="niki-old-qr-btn" style="margin-top:14px;">
79
- <i class="fa fa-qrcode"></i> (Eski) 250 Puan → QR Oluştur
80
- </button>
81
-
82
54
  <p style="font-size:12px; color:#ccc; margin-top:15px;">
83
55
  Niki The Cat Coffee &copy; Loyalty Program
84
56
  </p>
85
57
  </div>
86
58
  </div>
87
59
 
60
+ <!-- ✅ QR MODAL (2 dk) -->
61
+ <div id="modal-qr" class="qr-overlay">
62
+ <div class="ticket-card">
63
+ <div class="close-circle" onclick="closeQR()"><i class="fa fa-times"></i></div>
64
+
65
+ <div id="view-code">
66
+ <div class="ticket-top" id="ticket-title">KASAYA GÖSTERİNİZ</div>
67
+ <div class="ticket-body">
68
+ <div id="qrcode"></div>
69
+
70
+ <div style="font-size:14px; font-weight:800; color:#1a1a1a; margin-top:6px;" id="ticket-sub">
71
+ QR hazır — kasada okut.
72
+ </div>
73
+
74
+ <div class="timer-wrapper"><div class="timer-bar" id="time-bar"></div></div>
75
+ <div class="timer-text" id="timer-txt">2:00</div>
76
+
77
+ <div style="margin-top:10px; font-size:11px; color:#888;">
78
+ Okutma işlemi tamamlanınca otomatik onay ekranına geçer.
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ <div id="view-success" class="success-view">
84
+ <div class="success-icon"><i class="fa fa-check"></i></div>
85
+ <h2 style="font-size:24px; color:#1a1a1a; margin-bottom:10px;">AFİYET OLSUN!</h2>
86
+ <p style="color:#888; font-size:14px; margin-bottom:20px;">Kasa onayı alındı, puanın güncellendi.</p>
87
+ <button class="niki-btn" onclick="closeQR()" style="padding:15px; font-size:14px;">
88
+ TAMAM
89
+ </button>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
88
94
  <style>
89
- /* Bu küçük CSS'ler sadece yeni bölümleri düzgün gösterir */
90
- .niki-reward-stats{ margin-top: 14px; }
95
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap');
91
96
 
97
+ .niki-reward-stats{ margin-top: 14px; }
92
98
  .niki-reward-track{ height: 12px; border-radius: 999px; overflow: visible; }
93
99
  #niki-reward-fill{ height: 100%; border-radius: 999px; }
94
100
 
@@ -104,83 +110,10 @@
104
110
  background: rgba(255,255,255,.95);
105
111
  border-color: rgba(255,255,255,.95);
106
112
  }
107
-
108
- /* rewards container'ın içini client.js dolduruyor */
109
- .niki-mbar{ margin-bottom: 12px; }
110
- .niki-mbar-top{ display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; }
111
- .niki-mbar-title{ font-size:12px; font-weight:800; color:#fff; }
112
- .niki-mbar-val{ font-size:12px; font-weight:800; color:#d7d7d7; }
113
- .niki-mbar-track{ position:relative; height:10px; border-radius:999px; background: rgba(255,255,255,.10); overflow: hidden; }
114
- .niki-mbar-fill{ height:100%; border-radius:999px; background: rgba(255,255,255,.85); width:0%; }
115
- .niki-mbar-dot{
116
- position:absolute; top:50%; transform: translate(-50%,-50%);
117
- width:10px; height:10px; border-radius:999px;
118
- background: rgba(255,255,255,.18);
119
- border: 1px solid rgba(255,255,255,.28);
120
- }
121
- .niki-mbar-dot.on{ background: rgba(255,255,255,.95); border-color: rgba(255,255,255,.95); }
122
- .niki-mbar-hint{ margin-top:6px; font-size:11px; color:#a7a7a7; }
123
-
124
- .niki-r-list{ display:flex; flex-direction:column; gap:10px; }
125
- .niki-r-card{
126
- display:flex; justify-content:space-between; align-items:center;
127
- padding: 12px 12px;
128
- border-radius: 14px;
129
- background: rgba(255,255,255,.06);
130
- border: 1px solid rgba(255,255,255,.10);
131
- }
132
- .niki-r-card.on{
133
- background: rgba(255,255,255,.10);
134
- border-color: rgba(255,255,255,.18);
135
- }
136
- .niki-r-title{ font-weight:800; color:#fff; font-size:13px; }
137
- .niki-r-sub{ color:#bdbdbd; font-size:11px; margin-top:2px; }
138
- .niki-r-right{ display:flex; gap:10px; align-items:center; }
139
- .niki-r-badge{
140
- font-size:11px; font-weight:800;
141
- padding: 6px 10px;
142
- border-radius: 999px;
143
- background: rgba(255,255,255,.08);
144
- color: #cfcfcf;
145
- border: 1px solid rgba(255,255,255,.10);
146
- white-space: nowrap;
147
- }
148
- .niki-r-badge.on{
149
- background: rgba(255,255,255,.18);
150
- color:#fff;
151
- border-color: rgba(255,255,255,.22);
152
- }
153
-
154
- .niki-mini-btn{
155
- background: rgba(255,255,255,.10);
156
- border: 1px solid rgba(255,255,255,.18);
157
- color:#fff;
158
- padding: 6px 10px;
159
- border-radius: 10px;
160
- font-weight:800;
161
- font-size:12px;
162
- cursor:pointer;
163
- }
164
-
165
- .niki-coupon-card{
166
- padding: 14px;
167
- border-radius: 16px;
168
- background: rgba(255,255,255,.06);
169
- border: 1px solid rgba(255,255,255,.10);
170
- }
171
- .niki-qr-box{
172
- width: 100%;
173
- min-height: 220px;
174
- border-radius: 14px;
175
- display:flex; align-items:center; justify-content:center;
176
- background: rgba(0,0,0,.20);
177
- border: 1px dashed rgba(255,255,255,.18);
178
- }
179
113
  </style>
180
114
 
181
115
  <script>
182
- // Bu script sadece wallet sayfasında barı güncellemek için minik bir helper.
183
- // Asıl render/claim zaten güncel client.js içinde.
116
+ // sadece bar/dot görsel güncelleme (client.js wallet-data çekince zaten id'leri update ediyor)
184
117
  (function(){
185
118
  function applyRewardBar(data){
186
119
  try{
@@ -197,7 +130,6 @@
197
130
  const bm = document.getElementById('niki-bar-max');
198
131
  if (bm) bm.textContent = barMax;
199
132
 
200
- // dot highlight (60/120/180/250)
201
133
  document.querySelectorAll('.niki-ms-dot').forEach(dot=>{
202
134
  const at = Number(dot.getAttribute('data-at') || 0);
203
135
  if (points >= at) dot.classList.add('on');
@@ -206,33 +138,27 @@
206
138
  }catch(e){}
207
139
  }
208
140
 
209
- // client.js zaten wallet-data'yı window.__NIKI_WALLET_DATA__ içine koyuyor
141
+ // client.js her refreshte window.__NIKI_WALLET_DATA__ set ediyorsa kullan
210
142
  if (window.__NIKI_WALLET_DATA__) applyRewardBar(window.__NIKI_WALLET_DATA__);
211
143
 
212
- // ajaxify sonrası tekrar dene
213
144
  $(window).on('action:ajaxify.end', function(){
214
145
  if (window.__NIKI_WALLET_DATA__) applyRewardBar(window.__NIKI_WALLET_DATA__);
215
146
  });
216
147
 
217
- // claim sonrası event
218
- $(window).on('niki:coupon.ready', function(_, res){
219
- // İstersen burada QR üretip #niki-coupon-qr içine basarsın.
220
- // (QR üretim kütüphanen wallet sayfasında varsa entegre edebiliriz.)
221
- const area = document.getElementById('niki-coupon-area');
222
- if (area) area.style.display = 'block';
223
- const t = document.getElementById('niki-coupon-title');
224
- if (t && res && res.reward && res.reward.title) t.textContent = res.reward.title + ' • Kupon';
225
-
226
- // Şimdilik tokenı text olarak da gösterelim (QR yoksa bile)
227
- const qrBox = document.getElementById('niki-coupon-qr');
228
- if (qrBox && res && res.token){
229
- qrBox.innerHTML = '<div style="text-align:center;color:#fff;font-weight:800;">TOKEN</div>'
230
- + '<div style="margin-top:6px;color:#cfcfcf;font-size:12px;word-break:break-all;">' + String(res.token) + '</div>';
148
+ // client.js QR oluşturunca şu event’i basıyor:
149
+ // $(window).trigger('niki:qr.open', [{ token, title }])
150
+ // Ben client.js’te bunu basmıyorum çünkü modalı direkt client.js açıyor.
151
+ // Ama olur da event’le açmak istersen aşağıyı kullanabilirsin:
152
+ $(window).on('niki:qr.open', function(_, payload){
153
+ if(!payload || !payload.token) return;
154
+ if(typeof window.openQRWithToken === 'function'){
155
+ window.openQRWithToken(payload.token, payload.title || 'Ödül');
231
156
  }
232
157
  });
233
-
234
- $('#niki-coupon-close').on('click', function(){
235
- $('#niki-coupon-area').hide();
236
- });
237
158
  })();
159
+
160
+ // ✅ client.js modalı açıyor ama tpl tarafında closeQR global kalsın
161
+ window.closeQR = window.closeQR || function(){
162
+ $('#modal-qr').fadeOut(200);
163
+ };
238
164
  </script>