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 +105 -95
- package/package.json +1 -1
- package/static/lib/client.js +349 -190
- package/templates/niki-wallet.tpl +57 -131
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
352
|
-
|
|
347
|
+
const staffOk = await isStaff(req.uid);
|
|
348
|
+
if (!staffOk) return res.status(403).json({ success: false, message: 'Yetkisiz' });
|
|
353
349
|
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
359
|
+
const customerUid = parseInt(coupon.ownerUid, 10);
|
|
360
|
+
const cost = parseInt(coupon.cost || 0, 10);
|
|
365
361
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
package/static/lib/client.js
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
$(document).ready(function () {
|
|
4
|
-
//
|
|
4
|
+
// =========================================
|
|
5
|
+
// AYARLAR
|
|
6
|
+
// =========================================
|
|
5
7
|
const NIKI_LOGO_URL = "https://i.ibb.co/nZvtpss/logo-placeholder.png";
|
|
6
8
|
|
|
7
|
-
//
|
|
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
|
|
48
|
-
|
|
49
|
-
$('#
|
|
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('&', '&')
|
|
204
|
-
.replaceAll('<', '<')
|
|
205
|
-
.replaceAll('>', '>')
|
|
206
|
-
.replaceAll('"', '"')
|
|
207
|
-
.replaceAll("'", ''');
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
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('&', '&')
|
|
438
|
+
.replaceAll('<', '<')
|
|
439
|
+
.replaceAll('>', '>')
|
|
440
|
+
.replaceAll('"', '"')
|
|
441
|
+
.replaceAll("'", ''');
|
|
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
|
|
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
|
-
|
|
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
|
|
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 © 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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
218
|
-
$(window).
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if
|
|
223
|
-
|
|
224
|
-
|
|
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>
|