nodebb-plugin-niki-loyalty 1.0.22 → 1.0.24
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 +144 -95
- package/package.json +1 -1
- package/static/lib/client.js +368 -188
- package/templates/niki-wallet.tpl +63 -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
|
|
@@ -413,6 +423,45 @@ Plugin.init = async function (params) {
|
|
|
413
423
|
return res.status(500).json({ success: false, message: 'Sunucu hatası' });
|
|
414
424
|
}
|
|
415
425
|
});
|
|
426
|
+
const TIERS = [60, 120, 180, 250];
|
|
427
|
+
|
|
428
|
+
function pickTier(points) {
|
|
429
|
+
// en büyük uygun tier
|
|
430
|
+
let chosen = 0;
|
|
431
|
+
for (const t of TIERS) if (points >= t) chosen = t;
|
|
432
|
+
return chosen; // 0 ise yetersiz
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
router.post('/api/niki-loyalty/claim-auto', middleware.ensureLoggedIn, async (req, res) => {
|
|
436
|
+
try {
|
|
437
|
+
const uid = req.uid;
|
|
438
|
+
const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
|
|
439
|
+
|
|
440
|
+
if (TEST_MODE_UNLIMITED) {
|
|
441
|
+
// testte 250 gibi davran
|
|
442
|
+
const token = makeToken();
|
|
443
|
+
await db.set(`niki:coupon:${token}`, safeStringify({ token, ts: Date.now(), ownerUid: uid, cost: 250 }));
|
|
444
|
+
await db.expire(`niki:coupon:${token}`, 120);
|
|
445
|
+
return res.json({ success: true, token, cost: 250 });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const cost = pickTier(points);
|
|
449
|
+
if (!cost) return res.json({ success: false, message: 'En az 60 puan gerekli.' });
|
|
450
|
+
|
|
451
|
+
const token = makeToken();
|
|
452
|
+
await db.set(`niki:coupon:${token}`, safeStringify({
|
|
453
|
+
token,
|
|
454
|
+
ts: Date.now(),
|
|
455
|
+
ownerUid: uid,
|
|
456
|
+
cost, // ✅ kasada düşülecek miktar
|
|
457
|
+
}));
|
|
458
|
+
await db.expire(`niki:coupon:${token}`, 120); // ✅ 2 dk
|
|
459
|
+
|
|
460
|
+
return res.json({ success: true, token, cost });
|
|
461
|
+
} catch (e) {
|
|
462
|
+
return res.status(500).json({ success: false, message: 'Sunucu hatası' });
|
|
463
|
+
}
|
|
464
|
+
});
|
|
416
465
|
|
|
417
466
|
router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
|
|
418
467
|
try {
|
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,39 @@ $(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
|
}
|
|
64
|
+
$(document).on('click', '#btn-qr-auto', function () {
|
|
65
|
+
$.post('/api/niki-loyalty/claim-auto', { _csrf: config.csrf_token }, function (res) {
|
|
66
|
+
if (!res || !res.success) {
|
|
67
|
+
app.alertError(res?.message || 'QR oluşturulamadı');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
51
70
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
}
|
|
71
|
+
// modal aç (tpl’deki openCouponModal varsa onu çağır)
|
|
72
|
+
const title = `ÖDÜL • ${res.cost} PUAN`;
|
|
73
|
+
if (typeof window.openCouponModal === 'function') {
|
|
74
|
+
window.openCouponModal(title, res.token);
|
|
75
|
+
} else {
|
|
76
|
+
// client.js içindeki openQRModal fallback’ı varsa kullan
|
|
77
|
+
openQRModal({ token: res.token, title, ttlSeconds: 120 });
|
|
78
|
+
}
|
|
199
79
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
.replaceAll('>', '>')
|
|
206
|
-
.replaceAll('"', '"')
|
|
207
|
-
.replaceAll("'", ''');
|
|
208
|
-
}
|
|
209
|
-
function escapeAttr(str) {
|
|
210
|
-
return escapeHtml(str).replaceAll(' ', '');
|
|
211
|
-
}
|
|
80
|
+
// ✅ polling: puan düşünce success’e geç
|
|
81
|
+
initialPointsForQR = parseInt(localStorage.getItem('niki_last_points') || '0', 10) || null;
|
|
82
|
+
startPollingForSuccess();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
212
85
|
|
|
213
|
-
// -------------------------
|
|
214
|
-
// 1) Widget Başlatma ve Veri Yönetimi
|
|
215
|
-
// -------------------------
|
|
216
86
|
function initNikiWidget() {
|
|
217
87
|
if (!app.user.uid || app.user.uid <= 0) return;
|
|
218
88
|
|
|
219
|
-
// Widget yoksa ekle
|
|
220
89
|
if ($('#niki-floating-widget').length === 0) {
|
|
221
90
|
$('body').append(widgetHtml);
|
|
222
91
|
}
|
|
223
92
|
|
|
224
|
-
// Cache: anında göster
|
|
225
93
|
const cachedPoints = localStorage.getItem('niki_last_points');
|
|
226
94
|
if (cachedPoints !== null) {
|
|
227
95
|
$('#niki-live-points').text(cachedPoints);
|
|
@@ -230,25 +98,38 @@ $(document).ready(function () {
|
|
|
230
98
|
|
|
231
99
|
fixLogo();
|
|
232
100
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
101
|
+
$.get('/api/niki-loyalty/wallet-data', function (data) {
|
|
102
|
+
const freshPoints = data?.points || 0;
|
|
103
|
+
setPointsUI(freshPoints);
|
|
236
104
|
fixLogo();
|
|
105
|
+
|
|
106
|
+
// Wallet sayfasındaysa rewards render et
|
|
107
|
+
if (isWalletPage()) {
|
|
108
|
+
renderWalletFromData(data);
|
|
109
|
+
}
|
|
110
|
+
}).fail(function () {
|
|
111
|
+
if (cachedPoints === null) {
|
|
112
|
+
setPointsUI(0);
|
|
113
|
+
}
|
|
237
114
|
});
|
|
238
115
|
}
|
|
239
116
|
|
|
240
|
-
|
|
117
|
+
function isWalletPage() {
|
|
118
|
+
// nodebb template adı her projede değişebilir; pathname ile de kontrol edelim
|
|
119
|
+
const p = window.location.pathname || '';
|
|
120
|
+
return (ajaxify?.data?.template === 'niki-wallet') || p.indexOf('niki-wallet') > -1;
|
|
121
|
+
}
|
|
122
|
+
|
|
241
123
|
initNikiWidget();
|
|
242
124
|
|
|
243
|
-
// Sayfa geçişlerinde tekrar çalıştır
|
|
244
125
|
$(window).on('action:ajaxify.end', function () {
|
|
245
126
|
initNikiWidget();
|
|
246
127
|
setTimeout(fixLogo, 400);
|
|
247
128
|
});
|
|
248
129
|
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
130
|
+
// =========================================
|
|
131
|
+
// HEARTBEAT
|
|
132
|
+
// =========================================
|
|
252
133
|
let activeSeconds = 0;
|
|
253
134
|
let isUserActive = false;
|
|
254
135
|
let idleTimer;
|
|
@@ -261,7 +142,7 @@ $(document).ready(function () {
|
|
|
261
142
|
$(window).on('mousemove scroll keydown click touchstart', resetIdleTimer);
|
|
262
143
|
|
|
263
144
|
setInterval(() => {
|
|
264
|
-
if (ajaxify
|
|
145
|
+
if (ajaxify?.data?.template?.topic && document.visibilityState === 'visible' && isUserActive) {
|
|
265
146
|
activeSeconds++;
|
|
266
147
|
}
|
|
267
148
|
if (activeSeconds >= 60) {
|
|
@@ -274,19 +155,318 @@ $(document).ready(function () {
|
|
|
274
155
|
$.post('/api/niki-loyalty/heartbeat', { _csrf: config.csrf_token }, function (res) {
|
|
275
156
|
if (res && res.earned) {
|
|
276
157
|
setPointsUI(res.total);
|
|
277
|
-
|
|
278
158
|
showNikiToast(`+${res.points} Puan Kazandın! ⚡`, 'ok');
|
|
279
159
|
|
|
280
160
|
$('#niki-floating-widget').addClass('niki-bounce');
|
|
281
161
|
setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
|
|
282
162
|
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
fetchWalletData({ renderRewards: true });
|
|
163
|
+
// wallet açıksa güncelle
|
|
164
|
+
if (isWalletPage()) {
|
|
165
|
+
refreshWallet();
|
|
287
166
|
}
|
|
288
167
|
}
|
|
289
168
|
});
|
|
290
169
|
}
|
|
170
|
+
|
|
171
|
+
// =========================================
|
|
172
|
+
// WALLET: Rewards render + QR flow (2 dk)
|
|
173
|
+
// =========================================
|
|
174
|
+
let pollInterval = null;
|
|
175
|
+
let initialPointsForQR = null;
|
|
176
|
+
let timerInterval = null;
|
|
177
|
+
|
|
178
|
+
function refreshWallet() {
|
|
179
|
+
$.get('/api/niki-loyalty/wallet-data', function (data) {
|
|
180
|
+
setPointsUI(data?.points || 0);
|
|
181
|
+
renderWalletFromData(data);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function renderWalletFromData(data) {
|
|
186
|
+
// 1) Bar (varsa)
|
|
187
|
+
try {
|
|
188
|
+
const p = parseInt(data?.points || 0, 10) || 0;
|
|
189
|
+
const goal = parseInt(data?.barMax || 250, 10) || 250;
|
|
190
|
+
const pct = Math.min(100, (p / goal) * 100);
|
|
191
|
+
$('#prog-bar').css('width', pct + '%');
|
|
192
|
+
$('#target-txt').text(`${p} / ${goal}`);
|
|
193
|
+
} catch (e) {}
|
|
194
|
+
|
|
195
|
+
// 2) Rewards container varsa bas
|
|
196
|
+
const $wrap = $('#niki-rewards');
|
|
197
|
+
if ($wrap.length === 0) return;
|
|
198
|
+
|
|
199
|
+
const points = parseInt(data?.points || 0, 10) || 0;
|
|
200
|
+
const rewards = Array.isArray(data?.rewards) && data.rewards.length ? data.rewards : REWARDS_FALLBACK.map(r => ({
|
|
201
|
+
id: r.id, at: r.at, title: r.title, unlocked: points >= r.at
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
const cards = rewards.map(r => {
|
|
205
|
+
const at = parseInt(r.at || 0, 10) || 0;
|
|
206
|
+
const unlocked = points >= at;
|
|
207
|
+
|
|
208
|
+
const btnText = unlocked ? 'QR OLUŞTUR' : `${at - points} PUAN EKSİK`;
|
|
209
|
+
const btnDisabled = unlocked ? '' : 'disabled';
|
|
210
|
+
|
|
211
|
+
return `
|
|
212
|
+
<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;">
|
|
213
|
+
<div style="text-align:left;">
|
|
214
|
+
<div style="font-weight:800;color:#1a1a1a;font-size:13px;">${escapeHtml(r.title)}</div>
|
|
215
|
+
<div style="color:#888;font-size:11px;font-weight:700;margin-top:2px;">${at} puan</div>
|
|
216
|
+
</div>
|
|
217
|
+
<button class="niki-genqr-btn" data-reward="${escapeAttr(r.id)}" data-title="${escapeAttr(r.title)}" data-cost="${at}"
|
|
218
|
+
${btnDisabled}
|
|
219
|
+
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;">
|
|
220
|
+
${btnText}
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
223
|
+
`;
|
|
224
|
+
}).join('');
|
|
225
|
+
|
|
226
|
+
$wrap.html(`
|
|
227
|
+
<div style="font-size:11px;color:#888;text-align:left;margin:6px 0 10px;font-weight:700;">
|
|
228
|
+
QR oluşturunca puanın <b>kasada okutulunca</b> düşer. (2 dakika geçerli)
|
|
229
|
+
</div>
|
|
230
|
+
${cards}
|
|
231
|
+
`);
|
|
232
|
+
|
|
233
|
+
// click bind
|
|
234
|
+
$wrap.off('click.nikiGen').on('click.nikiGen', '.niki-genqr-btn:not([disabled])', function () {
|
|
235
|
+
const rewardId = $(this).data('reward');
|
|
236
|
+
const title = $(this).data('title') || 'Ödül';
|
|
237
|
+
const cost = parseInt($(this).data('cost') || 0, 10) || 0;
|
|
238
|
+
|
|
239
|
+
generateRewardQR({ rewardId, title, cost });
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function generateRewardQR({ rewardId, title, cost }) {
|
|
244
|
+
// qr modal varsa onu kullan, yoksa basit popup yap
|
|
245
|
+
if (!rewardId) return;
|
|
246
|
+
|
|
247
|
+
// güvenli: o anda puan snapshot
|
|
248
|
+
$.get('/api/niki-loyalty/wallet-data', function (data) {
|
|
249
|
+
const points = parseInt(data?.points || 0, 10) || 0;
|
|
250
|
+
if (points < cost) {
|
|
251
|
+
showNikiToast('Yetersiz puan', 'warn');
|
|
252
|
+
refreshWallet();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!confirm(`${title} için QR oluşturulsun mu? (2 dakika geçerli)`)) return;
|
|
257
|
+
|
|
258
|
+
// ✅ Claim = sadece token üret (puan düşmez), okutunca düşer
|
|
259
|
+
$.post('/api/niki-loyalty/claim', { rewardId, _csrf: config.csrf_token }, function (res) {
|
|
260
|
+
if (!res || !res.success || !res.token) {
|
|
261
|
+
showNikiToast(res?.message || 'QR oluşturulamadı', 'err');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
initialPointsForQR = points;
|
|
266
|
+
openQRModal({
|
|
267
|
+
token: res.token,
|
|
268
|
+
title: title,
|
|
269
|
+
ttlSeconds: QR_TTL_SECONDS,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
startPollingForSuccess();
|
|
273
|
+
}).fail(function () {
|
|
274
|
+
showNikiToast('Sunucu hatası (claim)', 'err');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function openQRModal({ token, title, ttlSeconds }) {
|
|
280
|
+
// Wallet tpl'nin modalını destekle
|
|
281
|
+
const $modal = $('#modal-qr');
|
|
282
|
+
const hasTplModal = $modal.length > 0;
|
|
283
|
+
|
|
284
|
+
if (!hasTplModal) {
|
|
285
|
+
// fallback: hızlı modal enjekte
|
|
286
|
+
injectFallbackQRModal();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// tpl modal elemanları varsa set
|
|
290
|
+
$('#view-code').show();
|
|
291
|
+
$('#view-success').hide();
|
|
292
|
+
$('#ticket-title').text((title || 'ÖDÜL') + ' • KASAYA GÖSTER');
|
|
293
|
+
$('#ticket-sub').text('QR’ı kasada okut.');
|
|
294
|
+
|
|
295
|
+
// QR bas (qrcodejs yoksa token yaz)
|
|
296
|
+
const qrEl = document.getElementById('qrcode');
|
|
297
|
+
if (qrEl) {
|
|
298
|
+
$(qrEl).empty();
|
|
299
|
+
|
|
300
|
+
if (typeof QRCode !== 'undefined') {
|
|
301
|
+
// Büyük, okunaklı QR
|
|
302
|
+
new QRCode(qrEl, {
|
|
303
|
+
text: String(token),
|
|
304
|
+
width: 260,
|
|
305
|
+
height: 260,
|
|
306
|
+
colorDark: "#000000",
|
|
307
|
+
colorLight: "#ffffff",
|
|
308
|
+
correctLevel: QRCode.CorrectLevel.L
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
$(qrEl).html(`<div style="font-weight:900;word-break:break-all;">${escapeHtml(token)}</div>`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// modal aç
|
|
316
|
+
$('#modal-qr').fadeIn(200).css('display', 'flex');
|
|
317
|
+
|
|
318
|
+
// timer
|
|
319
|
+
startTimer(ttlSeconds);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function startTimer(durationSec) {
|
|
323
|
+
clearInterval(timerInterval);
|
|
324
|
+
let timer = durationSec;
|
|
325
|
+
const total = durationSec;
|
|
326
|
+
|
|
327
|
+
$('#time-bar').css('width', '100%');
|
|
328
|
+
$('#timer-txt').text(formatTime(timer));
|
|
329
|
+
|
|
330
|
+
timerInterval = setInterval(function () {
|
|
331
|
+
// modal kapanırsa dur
|
|
332
|
+
if (!$('#modal-qr').is(':visible') || $('#view-success').is(':visible')) {
|
|
333
|
+
clearInterval(timerInterval);
|
|
334
|
+
timerInterval = null;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const pct = Math.max(0, (timer / total) * 100);
|
|
339
|
+
$('#time-bar').css('width', pct + '%');
|
|
340
|
+
$('#timer-txt').text(formatTime(timer));
|
|
341
|
+
|
|
342
|
+
timer -= 1;
|
|
343
|
+
if (timer < 0) {
|
|
344
|
+
clearInterval(timerInterval);
|
|
345
|
+
timerInterval = null;
|
|
346
|
+
closeQRModal();
|
|
347
|
+
showNikiToast('Süre doldu, kod geçersiz olabilir.', 'warn');
|
|
348
|
+
}
|
|
349
|
+
}, 1000);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function startPollingForSuccess() {
|
|
353
|
+
clearInterval(pollInterval);
|
|
354
|
+
|
|
355
|
+
pollInterval = setInterval(() => {
|
|
356
|
+
// modal açık değilse polling durdur
|
|
357
|
+
if (!$('#modal-qr').is(':visible')) {
|
|
358
|
+
clearInterval(pollInterval);
|
|
359
|
+
pollInterval = null;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ✅ Eski gibi: puan düşerse -> kasa okutmuş demektir
|
|
364
|
+
$.get('/api/niki-loyalty/wallet-data', function (data) {
|
|
365
|
+
const currentP = parseInt(data?.points || 0, 10) || 0;
|
|
366
|
+
|
|
367
|
+
// Puan düşmüşse başarı
|
|
368
|
+
if (initialPointsForQR != null && currentP < initialPointsForQR) {
|
|
369
|
+
clearInterval(pollInterval);
|
|
370
|
+
pollInterval = null;
|
|
371
|
+
|
|
372
|
+
showSuccessView();
|
|
373
|
+
setPointsUI(currentP);
|
|
374
|
+
// reward list refresh
|
|
375
|
+
renderWalletFromData(data);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}, 2000);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function showSuccessView() {
|
|
382
|
+
$('#view-code').hide();
|
|
383
|
+
$('#view-success').fadeIn(200);
|
|
384
|
+
|
|
385
|
+
// confetti varsa patlat
|
|
386
|
+
try {
|
|
387
|
+
if (typeof confetti !== 'undefined') {
|
|
388
|
+
confetti({ particleCount: 140, spread: 70, origin: { y: 0.6 } });
|
|
389
|
+
}
|
|
390
|
+
} catch (e) {}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// tpl'nin closeQR fonksiyonu varsa onu çağır, yoksa kendin kapat
|
|
394
|
+
window.closeQR = window.closeQR || function () { closeQRModal(); };
|
|
395
|
+
|
|
396
|
+
function closeQRModal() {
|
|
397
|
+
$('#modal-qr').fadeOut(200);
|
|
398
|
+
clearInterval(pollInterval);
|
|
399
|
+
pollInterval = null;
|
|
400
|
+
clearInterval(timerInterval);
|
|
401
|
+
timerInterval = null;
|
|
402
|
+
initialPointsForQR = null;
|
|
403
|
+
|
|
404
|
+
// wallet refresh
|
|
405
|
+
if (isWalletPage()) {
|
|
406
|
+
setTimeout(refreshWallet, 400);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function injectFallbackQRModal() {
|
|
411
|
+
if ($('#modal-qr').length) return;
|
|
412
|
+
|
|
413
|
+
$('body').append(`
|
|
414
|
+
<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;">
|
|
415
|
+
<div class="ticket-card" style="background:#fff;width:320px;border-radius:26px;overflow:hidden;position:relative;">
|
|
416
|
+
<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;">
|
|
417
|
+
<i class="fa fa-times"></i>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<div id="view-code">
|
|
421
|
+
<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;">
|
|
422
|
+
KASAYA GÖSTERİNİZ
|
|
423
|
+
</div>
|
|
424
|
+
<div class="ticket-body" style="padding:30px 22px;text-align:center;">
|
|
425
|
+
<div id="qrcode" style="display:flex;justify-content:center;margin-bottom:16px;"></div>
|
|
426
|
+
<div id="ticket-sub" style="font-size:13px;font-weight:900;color:#111;margin-top:4px;">QR hazır</div>
|
|
427
|
+
<div class="timer-wrapper" style="width:100%;height:6px;background:#eee;border-radius:10px;overflow:hidden;margin-top:12px;">
|
|
428
|
+
<div class="timer-bar" id="time-bar" style="height:100%;background:#C5A065;width:100%;"></div>
|
|
429
|
+
</div>
|
|
430
|
+
<div class="timer-text" id="timer-txt" style="font-size:12px;color:#777;margin-top:8px;font-weight:800;">2:00</div>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<div id="view-success" class="success-view" style="display:none;padding:34px 18px;text-align:center;">
|
|
435
|
+
<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;">
|
|
436
|
+
<i class="fa fa-check"></i>
|
|
437
|
+
</div>
|
|
438
|
+
<div style="font-size:20px;font-weight:900;color:#111;">AFİYET OLSUN!</div>
|
|
439
|
+
<div style="font-size:13px;color:#777;margin-top:6px;">Kasa onayı alındı.</div>
|
|
440
|
+
<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;">
|
|
441
|
+
TAMAM
|
|
442
|
+
</button>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function formatTime(sec) {
|
|
450
|
+
sec = Math.max(0, parseInt(sec, 10) || 0);
|
|
451
|
+
const m = Math.floor(sec / 60);
|
|
452
|
+
const s = sec % 60;
|
|
453
|
+
return `${m}:${s < 10 ? '0' + s : s}`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function escapeHtml(str) {
|
|
457
|
+
return String(str || '')
|
|
458
|
+
.replaceAll('&', '&')
|
|
459
|
+
.replaceAll('<', '<')
|
|
460
|
+
.replaceAll('>', '>')
|
|
461
|
+
.replaceAll('"', '"')
|
|
462
|
+
.replaceAll("'", ''');
|
|
463
|
+
}
|
|
464
|
+
function escapeAttr(str) {
|
|
465
|
+
return escapeHtml(str).replaceAll(' ', '');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Wallet sayfasına ilk girişte render
|
|
469
|
+
if (isWalletPage()) {
|
|
470
|
+
refreshWallet();
|
|
471
|
+
}
|
|
291
472
|
});
|
|
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,19 @@
|
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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>
|
|
36
|
+
<!-- ✅ REWARD KARTLARI (client.js buraya basacak: QR OLUŞTUR butonları) -->
|
|
37
|
+
<button id="btn-qr-auto" class="niki-btn">
|
|
38
|
+
<i class="fa fa-qrcode"></i> QR OLUŞTUR
|
|
39
|
+
</button>
|
|
49
40
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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>
|
|
41
|
+
<div style="font-size:11px; color:#888; margin-top:10px;">
|
|
42
|
+
QR oluşturduktan sonra <b>kasada okutulunca</b> puanın düşer. (QR 2 dakika geçerli)
|
|
43
|
+
</div>
|
|
60
44
|
|
|
61
|
-
<!-- ✅ Günlük kazanım aynı kaldı -->
|
|
62
45
|
<div class="niki-daily-stats" style="margin-top:18px;">
|
|
63
46
|
<div style="display:flex; justify-content:space-between; font-size:12px; color:#888; font-weight:700;">
|
|
64
47
|
<span>Günlük Kazanım</span>
|
|
@@ -74,21 +57,50 @@
|
|
|
74
57
|
</div>
|
|
75
58
|
</div>
|
|
76
59
|
|
|
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
60
|
<p style="font-size:12px; color:#ccc; margin-top:15px;">
|
|
83
61
|
Niki The Cat Coffee © Loyalty Program
|
|
84
62
|
</p>
|
|
85
63
|
</div>
|
|
86
64
|
</div>
|
|
87
65
|
|
|
66
|
+
<!-- ✅ QR MODAL (2 dk) -->
|
|
67
|
+
<div id="modal-qr" class="qr-overlay">
|
|
68
|
+
<div class="ticket-card">
|
|
69
|
+
<div class="close-circle" onclick="closeQR()"><i class="fa fa-times"></i></div>
|
|
70
|
+
|
|
71
|
+
<div id="view-code">
|
|
72
|
+
<div class="ticket-top" id="ticket-title">KASAYA GÖSTERİNİZ</div>
|
|
73
|
+
<div class="ticket-body">
|
|
74
|
+
<div id="qrcode"></div>
|
|
75
|
+
|
|
76
|
+
<div style="font-size:14px; font-weight:800; color:#1a1a1a; margin-top:6px;" id="ticket-sub">
|
|
77
|
+
QR hazır — kasada okut.
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="timer-wrapper"><div class="timer-bar" id="time-bar"></div></div>
|
|
81
|
+
<div class="timer-text" id="timer-txt">2:00</div>
|
|
82
|
+
|
|
83
|
+
<div style="margin-top:10px; font-size:11px; color:#888;">
|
|
84
|
+
Okutma işlemi tamamlanınca otomatik onay ekranına geçer.
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div id="view-success" class="success-view">
|
|
90
|
+
<div class="success-icon"><i class="fa fa-check"></i></div>
|
|
91
|
+
<h2 style="font-size:24px; color:#1a1a1a; margin-bottom:10px;">AFİYET OLSUN!</h2>
|
|
92
|
+
<p style="color:#888; font-size:14px; margin-bottom:20px;">Kasa onayı alındı, puanın güncellendi.</p>
|
|
93
|
+
<button class="niki-btn" onclick="closeQR()" style="padding:15px; font-size:14px;">
|
|
94
|
+
TAMAM
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
88
100
|
<style>
|
|
89
|
-
|
|
90
|
-
.niki-reward-stats{ margin-top: 14px; }
|
|
101
|
+
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap');
|
|
91
102
|
|
|
103
|
+
.niki-reward-stats{ margin-top: 14px; }
|
|
92
104
|
.niki-reward-track{ height: 12px; border-radius: 999px; overflow: visible; }
|
|
93
105
|
#niki-reward-fill{ height: 100%; border-radius: 999px; }
|
|
94
106
|
|
|
@@ -104,83 +116,10 @@
|
|
|
104
116
|
background: rgba(255,255,255,.95);
|
|
105
117
|
border-color: rgba(255,255,255,.95);
|
|
106
118
|
}
|
|
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
119
|
</style>
|
|
180
120
|
|
|
181
121
|
<script>
|
|
182
|
-
//
|
|
183
|
-
// Asıl render/claim zaten güncel client.js içinde.
|
|
122
|
+
// ✅ sadece bar/dot görsel güncelleme (client.js wallet-data çekince zaten id'leri update ediyor)
|
|
184
123
|
(function(){
|
|
185
124
|
function applyRewardBar(data){
|
|
186
125
|
try{
|
|
@@ -197,7 +136,6 @@
|
|
|
197
136
|
const bm = document.getElementById('niki-bar-max');
|
|
198
137
|
if (bm) bm.textContent = barMax;
|
|
199
138
|
|
|
200
|
-
// dot highlight (60/120/180/250)
|
|
201
139
|
document.querySelectorAll('.niki-ms-dot').forEach(dot=>{
|
|
202
140
|
const at = Number(dot.getAttribute('data-at') || 0);
|
|
203
141
|
if (points >= at) dot.classList.add('on');
|
|
@@ -206,33 +144,27 @@
|
|
|
206
144
|
}catch(e){}
|
|
207
145
|
}
|
|
208
146
|
|
|
209
|
-
// client.js
|
|
147
|
+
// client.js her refreshte window.__NIKI_WALLET_DATA__ set ediyorsa kullan
|
|
210
148
|
if (window.__NIKI_WALLET_DATA__) applyRewardBar(window.__NIKI_WALLET_DATA__);
|
|
211
149
|
|
|
212
|
-
// ajaxify sonrası tekrar dene
|
|
213
150
|
$(window).on('action:ajaxify.end', function(){
|
|
214
151
|
if (window.__NIKI_WALLET_DATA__) applyRewardBar(window.__NIKI_WALLET_DATA__);
|
|
215
152
|
});
|
|
216
153
|
|
|
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>';
|
|
154
|
+
// client.js QR oluşturunca şu event’i basıyor:
|
|
155
|
+
// $(window).trigger('niki:qr.open', [{ token, title }])
|
|
156
|
+
// Ben client.js’te bunu basmıyorum çünkü modalı direkt client.js açıyor.
|
|
157
|
+
// Ama olur da event’le açmak istersen aşağıyı kullanabilirsin:
|
|
158
|
+
$(window).on('niki:qr.open', function(_, payload){
|
|
159
|
+
if(!payload || !payload.token) return;
|
|
160
|
+
if(typeof window.openQRWithToken === 'function'){
|
|
161
|
+
window.openQRWithToken(payload.token, payload.title || 'Ödül');
|
|
231
162
|
}
|
|
232
163
|
});
|
|
233
|
-
|
|
234
|
-
$('#niki-coupon-close').on('click', function(){
|
|
235
|
-
$('#niki-coupon-area').hide();
|
|
236
|
-
});
|
|
237
164
|
})();
|
|
165
|
+
|
|
166
|
+
// ✅ client.js modalı açıyor ama tpl tarafında closeQR global kalsın
|
|
167
|
+
window.closeQR = window.closeQR || function(){
|
|
168
|
+
$('#modal-qr').fadeOut(200);
|
|
169
|
+
};
|
|
238
170
|
</script>
|