nodebb-plugin-niki-loyalty 1.0.29 → 1.0.30

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.
Files changed (3) hide show
  1. package/library.js +179 -153
  2. package/package.json +1 -1
  3. package/static/lib/client.js +162 -104
package/library.js CHANGED
@@ -2,21 +2,30 @@
2
2
 
3
3
  const db = require.main.require('./src/database');
4
4
  const user = require.main.require('./src/user');
5
+ const posts = require.main.require('./src/posts');
5
6
  const routeHelpers = require.main.require('./src/controllers/helpers');
6
7
  const nconf = require.main.require('nconf');
7
8
 
8
9
  const Plugin = {};
9
10
 
10
11
  // =========================
11
- // SETTINGS & REWARDS
12
+ // ⚙️ AYARLAR & KURALLAR (GAME LOGIC)
12
13
  // =========================
13
14
  const SETTINGS = {
14
- pointsPerHeartbeat: 5,
15
- dailyCap: 250,
16
- // coffeeCost: 250, // (eski kodda vardı ama artık tier sistemi var; istersen kullanırız)
15
+ dailyCap: 28, // Günlük Maksimum Puan
17
16
  };
18
17
 
19
- // Rewards configuration (Ordered from highest cost to lowest)
18
+ // Puan Tablosu ve Limitleri
19
+ const ACTIONS = {
20
+ login: { points: 2, limit: 1, name: 'Günlük Giriş 👋' },
21
+ new_topic: { points: 7, limit: 1, name: 'Yeni Konu 📝' },
22
+ reply: { points: 3.5, limit: 2, name: 'Yorum Yazma 💬' },
23
+ read: { points: 1, limit: 8, name: 'Konu Okuma 👀' }, // Heartbeat ile çalışır
24
+ like_given: { points: 1, limit: 2, name: 'Beğeni Atma ❤️' },
25
+ like_taken: { points: 1, limit: 2, name: 'Beğeni Alma 🌟' }
26
+ };
27
+
28
+ // Ödüller
20
29
  const REWARDS = [
21
30
  { cost: 250, name: 'Ücretsiz Kahve ☕' },
22
31
  { cost: 180, name: '%60 İndirimli Kahve' },
@@ -24,43 +33,23 @@ const REWARDS = [
24
33
  { cost: 60, name: '1 Kurabiye 🍪' },
25
34
  ];
26
35
 
27
- // TEST MODE (Set to true to bypass point checks)
28
36
  const TEST_MODE_UNLIMITED = false;
29
37
 
30
38
  // =========================
31
- // HELPER FUNCTIONS
39
+ // 🛠 YARDIMCI FONKSİYONLAR
32
40
  // =========================
33
41
  function safeParseMaybeJson(x) {
34
42
  if (x == null) return null;
35
43
  if (typeof x === 'object') return x;
36
- if (typeof x === 'string') {
37
- try {
38
- return JSON.parse(x);
39
- } catch (e) {
40
- return null;
41
- }
42
- }
43
- return null;
44
+ try { return JSON.parse(x); } catch (e) { return null; }
44
45
  }
45
46
 
46
47
  function safeStringify(obj) {
47
- try {
48
- return JSON.stringify(obj);
49
- } catch (e) {
50
- return null;
51
- }
48
+ try { return JSON.stringify(obj); } catch (e) { return null; }
52
49
  }
53
50
 
54
- // =========================
55
- // LOGGING
56
- // =========================
57
51
  async function addUserLog(uid, type, amount, desc) {
58
- const logEntry = {
59
- ts: Date.now(),
60
- type, // 'earn' | 'spend'
61
- amt: amount,
62
- txt: desc,
63
- };
52
+ const logEntry = { ts: Date.now(), type, amt: amount, txt: desc };
64
53
  const payload = safeStringify(logEntry);
65
54
  if (!payload) return;
66
55
  await db.listAppend(`niki:activity:${uid}`, payload);
@@ -68,13 +57,8 @@ async function addUserLog(uid, type, amount, desc) {
68
57
  }
69
58
 
70
59
  async function addKasaLog(staffUid, customerName, customerUid, rewardName, amount) {
71
- const logEntry = {
72
- ts: Date.now(),
73
- staff: staffUid,
74
- cust: customerName,
75
- cuid: customerUid,
76
- amt: amount,
77
- reward: rewardName, // Store the specific reward name
60
+ const logEntry = {
61
+ ts: Date.now(), staff: staffUid, cust: customerName, cuid: customerUid, amt: amount, reward: rewardName
78
62
  };
79
63
  const payload = safeStringify(logEntry);
80
64
  if (!payload) return;
@@ -82,61 +66,138 @@ async function addKasaLog(staffUid, customerName, customerUid, rewardName, amoun
82
66
  await db.listTrim('niki:kasa:history', -100, -1);
83
67
  }
84
68
 
69
+ // 🔥 MERKEZİ PUAN DAĞITIM FONKSİYONU 🔥
70
+ // Bütün puan işlemleri buradan geçer, limitleri kontrol eder.
71
+ async function awardDailyAction(uid, actionKey) {
72
+ try {
73
+ const today = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
74
+ const rule = ACTIONS[actionKey];
75
+
76
+ if (!rule) return;
77
+
78
+ // 1. Genel Günlük Limit Kontrolü (28 Puan)
79
+ const dailyScoreKey = `niki:daily:${uid}:${today}`;
80
+ const currentDailyScore = parseFloat((await db.getObjectField(dailyScoreKey, 'score')) || 0);
81
+
82
+ if (currentDailyScore >= SETTINGS.dailyCap) return; // Günlük limit doldu
83
+
84
+ // 2. Eylem Bazlı Limit Kontrolü (Örn: Max 2 Yorum)
85
+ const actionCountKey = `niki:daily:${uid}:${today}:counts`;
86
+ const currentActionCount = parseInt((await db.getObjectField(actionCountKey, actionKey)) || 0, 10);
87
+
88
+ if (currentActionCount >= rule.limit) return; // Bu eylemin hakkı doldu
89
+
90
+ // 3. Puan Ver ve Kaydet
91
+ let pointsToGive = rule.points;
92
+
93
+ // Eğer verilecek puan 28 sınırını aşıyorsa, sadece aradaki farkı ver
94
+ if (currentDailyScore + pointsToGive > SETTINGS.dailyCap) {
95
+ pointsToGive = SETTINGS.dailyCap - currentDailyScore;
96
+ }
97
+
98
+ if (pointsToGive <= 0) return;
99
+
100
+ // DB Güncellemeleri
101
+ await user.incrementUserFieldBy(uid, 'niki_points', pointsToGive); // Cüzdan
102
+ await db.incrObjectFieldBy(dailyScoreKey, 'score', pointsToGive); // Günlük Toplam
103
+ await db.incrObjectFieldBy(actionCountKey, actionKey, 1); // Eylem Sayacı
104
+
105
+ // Logla
106
+ await addUserLog(uid, 'earn', pointsToGive, rule.name);
107
+
108
+ } catch (err) {
109
+ console.error(`[Niki-Loyalty] Error awarding points for ${actionKey}:`, err);
110
+ }
111
+ }
112
+
113
+
114
+ // =========================
115
+ // ⚓ HOOKS (Olay Dinleyicileri)
116
+ // =========================
117
+
118
+ // 1. GÜNLÜK GİRİŞ (Login)
119
+ Plugin.onLogin = async function (data) {
120
+ if (!data || !data.uid) return;
121
+ await awardDailyAction(data.uid, 'login');
122
+ };
123
+
124
+ // 2. YENİ KONU AÇMA
125
+ Plugin.onTopicCreate = async function (data) {
126
+ // data.topic.uid konusuyu açan kişidir
127
+ if (!data || !data.topic || !data.topic.uid) return;
128
+ await awardDailyAction(data.topic.uid, 'new_topic');
129
+ };
130
+
131
+ // 3. YORUM YAZMA (Reply)
132
+ Plugin.onPostCreate = async function (data) {
133
+ if (!data || !data.post || !data.post.uid) return;
134
+
135
+ // Eğer post "main" ise (yani konunun kendisi ise) yorum sayılmaz, konu sayılır.
136
+ // NodeBB'de isMain kontrolü:
137
+ const isMain = await posts.isMain(data.post.pid);
138
+ if (isMain) return; // Bunu TopicCreate zaten yakalıyor
139
+
140
+ await awardDailyAction(data.post.uid, 'reply');
141
+ };
142
+
143
+ // 4. BEĞENİ (Like Atma ve Alma)
144
+ Plugin.onUpvote = async function (data) {
145
+ // data = { post: { pid, uid, ... }, uid: <like atan>, ... }
146
+ // Like Atan Kazanır:
147
+ if (data.uid) {
148
+ await awardDailyAction(data.uid, 'like_given');
149
+ }
150
+
151
+ // Like Alan Kazanır (Post sahibi):
152
+ if (data.post && data.post.uid && data.post.uid !== data.uid) {
153
+ // Kendine like atarsa kazanmasın
154
+ await awardDailyAction(data.post.uid, 'like_taken');
155
+ }
156
+ };
157
+
158
+
85
159
  // =========================
86
- // PLUGIN INIT
160
+ // 🚀 INIT & ROUTES
87
161
  // =========================
88
162
  Plugin.init = async function (params) {
89
163
  const router = params.router;
90
164
  const middleware = params.middleware;
91
165
 
92
- // 1) HEARTBEAT (Earn Points) + ✅ EARN LOG (2. koddaki özellik)
166
+ // 1) HEARTBEAT (Artık "Okuma" Puanı veriyor)
167
+ // Client-side script her 30-60 saniyede bir bu adrese istek atmalıdır.
93
168
  router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
94
169
  try {
95
170
  const uid = req.uid;
96
- const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
97
- const dailyKey = `niki:daily:${uid}:${today}`;
98
-
99
- const currentDailyScore = parseInt((await db.getObjectField(dailyKey, 'score')) || 0, 10);
100
-
101
- if (currentDailyScore >= SETTINGS.dailyCap) {
102
- return res.json({ earned: false, reason: 'daily_cap' });
103
- }
104
-
105
- await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
106
- await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
107
-
108
- // ✅ Log: Cüzdan geçmişinde "kazanım" görünsün
109
- await addUserLog(uid, 'earn', SETTINGS.pointsPerHeartbeat, 'Ders Çalışma 📚');
110
-
171
+ // Heartbeat geldiğinde "read" aksiyonunu tetikle
172
+ await awardDailyAction(uid, 'read');
173
+
111
174
  const newBalance = await user.getUserField(uid, 'niki_points');
112
- return res.json({ earned: true, points: SETTINGS.pointsPerHeartbeat, total: newBalance });
175
+ return res.json({ earned: true, total: newBalance });
113
176
  } catch (err) {
114
- return res.status(500).json({ earned: false, reason: 'server_error' });
177
+ return res.status(500).json({ error: 'error' });
115
178
  }
116
179
  });
117
180
 
118
- // 2) WALLET DATA (Data + History + Rewards Info)
181
+ // 2) WALLET DATA (Cüzdan Bilgileri)
119
182
  router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
120
183
  try {
121
184
  const uid = req.uid;
122
185
  const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
123
-
186
+
124
187
  const [userData, dailyData, historyRaw] = await Promise.all([
125
188
  user.getUserFields(uid, ['niki_points']),
126
189
  db.getObject(`niki:daily:${uid}:${today}`),
127
190
  db.getListRange(`niki:activity:${uid}`, 0, -1),
128
191
  ]);
129
192
 
130
- const currentPoints = parseInt(userData?.niki_points || 0, 10);
131
- const dailyScore = parseInt(dailyData?.score || 0, 10);
132
-
193
+ const dailyScore = parseFloat(dailyData?.score || 0);
133
194
  let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
134
195
  if (dailyPercent > 100) dailyPercent = 100;
135
196
 
136
197
  const history = (historyRaw || []).map(safeParseMaybeJson).filter(Boolean).reverse();
137
198
 
138
199
  return res.json({
139
- points: currentPoints,
200
+ points: parseInt(userData?.niki_points || 0, 10),
140
201
  dailyScore,
141
202
  dailyCap: SETTINGS.dailyCap,
142
203
  dailyPercent,
@@ -148,8 +209,9 @@ Plugin.init = async function (params) {
148
209
  }
149
210
  });
150
211
 
151
- // 3) KASA HISTORY (+ ✅ iconBg + reward fallback (2. koddaki özellik))
212
+ // 3) KASA HISTORY
152
213
  router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
214
+ // ... (Mevcut kodunun aynısı - sadece yetki kontrolü var)
153
215
  try {
154
216
  const isAdmin = await user.isAdministrator(req.uid);
155
217
  const isMod = await user.isGlobalModerator(req.uid);
@@ -157,121 +219,85 @@ Plugin.init = async function (params) {
157
219
 
158
220
  const raw = await db.getListRange('niki:kasa:history', 0, -1);
159
221
  const rows = (raw || []).map(safeParseMaybeJson).filter(Boolean).reverse();
160
-
222
+
223
+ // Kullanıcı detaylarını doldurma (Map logic)
161
224
  const uids = rows.map(r => parseInt(r.cuid, 10)).filter(n => Number.isFinite(n) && n > 0);
162
225
  const users = await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'icon:bgColor']);
163
226
  const userMap = {};
164
- (users || []).forEach(u => {
165
- userMap[u.uid] = u;
166
- });
227
+ (users || []).forEach(u => userMap[u.uid] = u);
167
228
 
168
229
  const rp = nconf.get('relative_path') || '';
169
-
170
230
  const enriched = rows.map(r => {
171
- const uid = parseInt(r.cuid, 10);
172
- const u = userMap[uid];
173
- if (!u) {
174
- // yine de reward fallback yapalım
175
- return { ...r, reward: r.reward || 'İşlem' };
176
- }
231
+ const u = userMap[r.cuid] || {};
177
232
  return {
178
233
  ...r,
179
234
  cust: u.username || r.cust || 'Bilinmeyen',
180
- userslug: u.userslug || r.userslug || '',
181
- picture: u.picture || r.picture || '',
182
- iconBg: u['icon:bgColor'] || r.iconBg || '#4b5563',
235
+ picture: u.picture || '',
236
+ iconBg: u['icon:bgColor'] || '#4b5563',
183
237
  profileUrl: u.userslug ? `${rp}/user/${u.userslug}` : '',
184
- reward: r.reward || 'İşlem',
238
+ reward: r.reward || 'İşlem'
185
239
  };
186
240
  });
187
-
188
241
  return res.json(enriched);
189
- } catch (err) {
190
- return res.status(500).json([]);
191
- }
242
+ } catch(e) { return res.status(500).json([]); }
192
243
  });
193
244
 
194
- // 4) GENERATE QR (Check if user has enough for MINIMUM reward)
245
+ // 4) QR OLUŞTURMA
195
246
  router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
196
247
  try {
197
- const uid = req.uid;
198
- const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
199
-
200
- const minCost = REWARDS[REWARDS.length - 1].cost;
201
-
202
- if (!TEST_MODE_UNLIMITED && points < minCost) {
203
- return res.json({ success: false, message: `En az ${minCost} puan gerekli.` });
204
- }
205
-
206
- const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
207
- await db.set(`niki:qr:${token}`, uid);
208
- await db.expire(`niki:qr:${token}`, 120);
248
+ const uid = req.uid;
249
+ const points = parseFloat((await user.getUserField(uid, 'niki_points')) || 0);
250
+ const minCost = REWARDS[REWARDS.length - 1].cost; // En ucuz ödül
209
251
 
210
- return res.json({ success: true, token });
211
- } catch (err) {
212
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
213
- }
252
+ if (!TEST_MODE_UNLIMITED && points < minCost) {
253
+ return res.json({ success: false, message: `Yetersiz Puan. En az ${minCost} gerekli.` });
254
+ }
255
+ const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
256
+ await db.set(`niki:qr:${token}`, uid);
257
+ await db.expire(`niki:qr:${token}`, 120); // 2 dakika geçerli
258
+ return res.json({ success: true, token });
259
+ } catch(e) { return res.status(500).json({ success: false }); }
214
260
  });
215
261
 
216
- // 5) SCAN QR (Determine Reward & Deduct Points)
262
+ // 5) QR TARATMA (Kasa İşlemi)
217
263
  router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
218
- try {
219
- const token = req.body && req.body.token ? String(req.body.token) : '';
220
- const isAdmin = await user.isAdministrator(req.uid);
221
- const isMod = await user.isGlobalModerator(req.uid);
222
- if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
223
-
224
- const customerUid = await db.get(`niki:qr:${token}`);
225
- if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
226
-
227
- const pts = parseInt((await user.getUserField(customerUid, 'niki_points')) || 0, 10);
228
-
229
- // Calculate best possible reward
230
- let selectedReward = null;
231
- if (!TEST_MODE_UNLIMITED) {
232
- for (const reward of REWARDS) {
233
- if (pts >= reward.cost) {
234
- selectedReward = reward;
235
- break;
236
- }
237
- }
238
- if (!selectedReward) {
239
- return res.json({ success: false, message: 'Puan Yetersiz' });
264
+ // ... (Mevcut kodunun aynısı)
265
+ try {
266
+ const token = req.body.token;
267
+ const isAdmin = await user.isAdministrator(req.uid);
268
+ const isMod = await user.isGlobalModerator(req.uid);
269
+ if (!isAdmin && !isMod) return res.status(403).json({success:false, message:'Yetkisiz'});
270
+
271
+ const custUid = await db.get(`niki:qr:${token}`);
272
+ if (!custUid) return res.json({success:false, message:'Geçersiz Kod'});
273
+
274
+ const pts = parseFloat(await user.getUserField(custUid, 'niki_points') || 0);
275
+
276
+ let selectedReward = null;
277
+ if (!TEST_MODE_UNLIMITED) {
278
+ for (const r of REWARDS) {
279
+ if (pts >= r.cost) { selectedReward = r; break; }
280
+ }
281
+ if (!selectedReward) return res.json({success:false, message:'Puan Yetersiz'});
282
+ } else { selectedReward = REWARDS[0]; }
283
+
284
+ if (!TEST_MODE_UNLIMITED) {
285
+ await user.decrementUserFieldBy(custUid, 'niki_points', selectedReward.cost);
240
286
  }
241
- } else {
242
- selectedReward = REWARDS[0];
243
- }
244
-
245
- // Deduct Points
246
- if (!TEST_MODE_UNLIMITED) {
247
- await user.decrementUserFieldBy(customerUid, 'niki_points', selectedReward.cost);
248
- }
249
-
250
- await db.delete(`niki:qr:${token}`);
251
-
252
- const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
253
-
254
- // Logs
255
- await addUserLog(customerUid, 'spend', selectedReward.cost, selectedReward.name);
256
- await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid, selectedReward.name, selectedReward.cost);
257
-
258
- return res.json({
259
- success: true,
260
- customer: customerData,
261
- rewardName: selectedReward.name,
262
- cost: selectedReward.cost,
263
- message: 'Onaylandı!',
264
- });
265
- } catch (err) {
266
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
267
- }
287
+ await db.delete(`niki:qr:${token}`);
288
+
289
+ const cData = await user.getUserFields(custUid, ['username', 'picture', 'userslug']);
290
+ await addUserLog(custUid, 'spend', selectedReward.cost, selectedReward.name);
291
+ await addKasaLog(req.uid, cData.username, custUid, selectedReward.name, selectedReward.cost);
292
+
293
+ return res.json({ success: true, customer: cData, rewardName: selectedReward.name, cost: selectedReward.cost });
294
+ } catch(e) { return res.status(500).json({success:false}); }
268
295
  });
269
296
 
270
- // 6) PAGE ROUTES
297
+ // 6) SAYFA ROTALARI
271
298
  routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
272
- const isAdmin = await user.isAdministrator(req.uid);
273
- const isMod = await user.isGlobalModerator(req.uid);
274
- if (!isAdmin && !isMod) return res.render('403', {});
299
+ const isStaff = await user.isAdministrator(req.uid) || await user.isGlobalModerator(req.uid);
300
+ if (!isStaff) return res.render('403', {});
275
301
  return res.render('niki-kasa', { title: 'Niki Kasa' });
276
302
  });
277
303
  };
@@ -292,4 +318,4 @@ Plugin.addNavigation = async function (nav) {
292
318
  return nav;
293
319
  };
294
320
 
295
- module.exports = Plugin;
321
+ module.exports = Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-niki-loyalty",
3
- "version": "1.0.29",
3
+ "version": "1.0.30",
4
4
  "description": "Niki The Cat Coffee Loyalty System - Earn points while studying on IEU Forum.",
5
5
  "main": "library.js",
6
6
  "nbbpm": {
@@ -1,128 +1,186 @@
1
1
  'use strict';
2
2
 
3
+ /* globals $, app, socket, ajaxify, utils */
4
+
3
5
  $(document).ready(function () {
4
- // --- AYARLAR ---
5
- // 1. Logo Ayarı (Senin çalışan linkin)
6
- const NIKI_LOGO_URL = "https://i.ibb.co/nZvtpss/logo-placeholder.png";
6
+ let heartbeatInterval = null;
7
+
8
+ // Puanları güzel göstermek için yardımcı fonksiyon (Örn: 10.0 -> 10, 10.5 -> 10.5)
9
+ function formatPoints(points) {
10
+ let val = parseFloat(points);
11
+ if (isNaN(val)) return '0';
12
+ // Eğer tam sayı ise virgülsüz, değilse 1 basamaklı göster
13
+ return Number.isInteger(val) ? val.toFixed(0) : val.toFixed(1);
14
+ }
7
15
 
8
- // Widget HTML Şablonu
9
- const widgetHtml = `
10
- <div id="niki-floating-widget" class="niki-hidden">
11
- <div class="niki-widget-content" onclick="ajaxify.go('niki-wallet')">
12
- <img src="${NIKI_LOGO_URL}" class="niki-widget-logo" alt="Niki">
13
- <div class="niki-widget-text">
14
- <span class="niki-lbl">PUANIM</span>
15
- <span class="niki-val" id="niki-live-points">...</span>
16
- </div>
17
- </div>
18
- </div>
19
- `;
16
+ $(window).on('action:ajaxify.end', function (ev, data) {
17
+ // 1. ÖNCEKİ SAYAÇLARI TEMİZLE (Sayfa geçişlerinde üst üste binmesin)
18
+ if (heartbeatInterval) {
19
+ clearInterval(heartbeatInterval);
20
+ heartbeatInterval = null;
21
+ }
20
22
 
21
- // 1. Widget Başlatma ve Veri Yönetimi
22
- function initNikiWidget() {
23
- if (!app.user.uid || app.user.uid <= 0) return;
23
+ // ============================================================
24
+ // ❤️ KONU OKUMA (HEARTBEAT) SİSTEMİ
25
+ // ============================================================
26
+ // Sadece 'topic' (konu) sayfasındaysak sayaç çalışsın.
27
+ if (ajaxify.data.template.name === 'topic') {
28
+ console.log('[Niki-Loyalty] Konu sayfası algılandı, sayaç başlatılıyor...');
29
+
30
+ // 30 Saniyede bir tetikle (Günde 8 limit var backendde)
31
+ heartbeatInterval = setInterval(function () {
32
+ // Tarayıcı sekmesi aktif değilse gönderme (opsiyonel optimizasyon)
33
+ if (document.hidden) return;
24
34
 
25
- // Widget yoksa ekle
26
- if ($('#niki-floating-widget').length === 0) {
27
- $('body').append(widgetHtml);
35
+ $.post('/api/niki-loyalty/heartbeat', {}, function (response) {
36
+ if (response && response.earned) {
37
+ // Eğer puan kazandıysa ufak bir bildirim göster
38
+ app.alert({
39
+ type: 'success',
40
+ title: 'Puan Kazanıldı!',
41
+ message: 'Konu okuduğun için puan hesabına eklendi. 🐈',
42
+ timeout: 2500
43
+ });
44
+ console.log('[Niki-Loyalty] Heartbeat başarılı. Yeni Puan:', response.total);
45
+ }
46
+ });
47
+ }, 30000); // 30.000 ms = 30 Saniye
28
48
  }
29
49
 
30
- // --- HIZLI YÜKLEME (CACHE) ---
31
- // Önce hafızadaki son puanı hemen göster (Bekletme yapmaz)
32
- const cachedPoints = localStorage.getItem('niki_last_points');
33
- if (cachedPoints !== null) {
34
- $('#niki-live-points').text(cachedPoints);
35
- $('#niki-floating-widget').removeClass('niki-hidden');
50
+ // ============================================================
51
+ // 💰 CÜZDAN SAYFASI (niki-wallet)
52
+ // ============================================================
53
+ if (data.url === 'niki-wallet') {
54
+ loadWalletData();
36
55
  }
37
56
 
38
- // Logo Kontrolü (Garanti olsun)
39
- fixLogo();
57
+ // ============================================================
58
+ // 🏪 KASA SAYFASI (niki-kasa) - Yetkili İçin
59
+ // ============================================================
60
+ if (data.url === 'niki-kasa') {
61
+ loadKasaHistory(); // Geçmişi yükle
62
+ setupKasaScanner(); // QR okutma butonlarını ayarla
63
+ }
64
+ });
40
65
 
41
- // --- GÜNCEL VERİ ÇEKME ---
42
- // Arka planda sunucuya sor: "Puan değişti mi?"
43
- $.get('/api/niki-loyalty/wallet-data', function(data) {
44
- const freshPoints = data.points || 0;
45
-
46
- // Puanı güncelle
47
- $('#niki-live-points').text(freshPoints);
48
- $('#niki-floating-widget').removeClass('niki-hidden'); // İlk kez açılıyorsa göster
66
+ // -------------------------------------------------------------
67
+ // CÜZDAN FONKSİYONLARI
68
+ // -------------------------------------------------------------
69
+ function loadWalletData() {
70
+ $.get('/api/niki-loyalty/wallet-data', function (data) {
71
+ // Puanları yerleştir (Decimal desteği ile)
72
+ $('#user-points').text(formatPoints(data.points));
73
+ $('#daily-score').text(formatPoints(data.dailyScore));
74
+ $('#daily-cap').text(data.dailyCap);
49
75
 
50
- // Yeni puanı hafızaya at (Bir sonraki giriş için)
51
- localStorage.setItem('niki_last_points', freshPoints);
76
+ // Progress Bar
77
+ const percent = data.dailyPercent > 100 ? 100 : data.dailyPercent;
78
+ $('#daily-progress').css('width', percent + '%').text(Math.round(percent) + '%');
79
+
80
+ // Geçmiş Tablosu
81
+ const historyList = $('#history-list');
82
+ historyList.empty();
52
83
 
53
- // Logoyu tekrar kontrol et (Resim geç yüklendiyse)
54
- fixLogo();
55
- }).fail(function() {
56
- // Hata olursa ve cache yoksa 0 yaz
57
- if (cachedPoints === null) {
58
- $('#niki-live-points').text('0');
59
- $('#niki-floating-widget').removeClass('niki-hidden');
84
+ if (data.history && data.history.length > 0) {
85
+ data.history.forEach(function (item) {
86
+ const colorClass = item.type === 'earn' ? 'text-success' : 'text-danger';
87
+ const sign = item.type === 'earn' ? '+' : '-';
88
+ const dateStr = new Date(item.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
89
+
90
+ const html = `
91
+ <li class="list-group-item d-flex justify-content-between align-items-center">
92
+ <div>
93
+ <small class="text-muted me-2">${dateStr}</small>
94
+ <span>${item.txt}</span>
95
+ </div>
96
+ <span class="fw-bold ${colorClass}">${sign}${formatPoints(item.amt)}</span>
97
+ </li>
98
+ `;
99
+ historyList.append(html);
100
+ });
101
+ } else {
102
+ historyList.append('<li class="list-group-item text-center text-muted">Henüz işlem yok.</li>');
60
103
  }
61
- });
62
- }
63
104
 
64
- // Logo Düzeltici (Senin çalışan kodun entegresi)
65
- function fixLogo() {
66
- const img = document.querySelector("img.niki-widget-logo");
67
- if (img && img.src !== NIKI_LOGO_URL) {
68
- img.src = NIKI_LOGO_URL;
69
- }
105
+ // QR Oluştur Butonu
106
+ $('#btn-generate-qr').off('click').on('click', function () {
107
+ $(this).prop('disabled', true);
108
+ $.post('/api/niki-loyalty/generate-qr', {}, function (res) {
109
+ $('#btn-generate-qr').prop('disabled', false);
110
+ if (res.success) {
111
+ // Basit bir modal veya alert ile kodu göster (veya QR kütüphanesi kullan)
112
+ // Şimdilik token'ı text olarak gösteriyoruz:
113
+ app.alert({
114
+ type: 'info',
115
+ title: 'Kod Oluşturuldu',
116
+ message: '<div class="text-center">Kasiyere bu kodu göster:<br><h2 style="margin:10px 0; letter-spacing:2px;">' + res.token + '</h2><small>2 dakika geçerli</small></div>',
117
+ timeout: 10000 // 10 saniye ekranda kalsın
118
+ });
119
+ } else {
120
+ app.alert({ type: 'danger', message: res.message });
121
+ }
122
+ });
123
+ });
124
+ });
70
125
  }
71
126
 
72
- // Başlat
73
- initNikiWidget();
74
-
75
- // Sayfa Geçişlerinde Tekrar Çalıştır
76
- $(window).on('action:ajaxify.end', function () {
77
- initNikiWidget();
78
- setTimeout(fixLogo, 500); // 0.5sn sonra son bir kontrol
79
- });
80
-
81
- // --- AKTİFLİK SİSTEMİ (Heartbeat) ---
82
- let activeSeconds = 0;
83
- let isUserActive = false;
84
- let idleTimer;
85
-
86
- function resetIdleTimer() {
87
- isUserActive = true;
88
- clearTimeout(idleTimer);
89
- idleTimer = setTimeout(() => { isUserActive = false; }, 30000);
90
- }
91
- $(window).on('mousemove scroll keydown click touchstart', resetIdleTimer);
127
+ // -------------------------------------------------------------
128
+ // KASA FONKSİYONLARI (Admin/Mod)
129
+ // -------------------------------------------------------------
130
+ function loadKasaHistory() {
131
+ const tbody = $('#kasa-history-tbody');
132
+ if (tbody.length === 0) return;
92
133
 
93
- setInterval(() => {
94
- if (ajaxify.data.template.topic && document.visibilityState === 'visible' && isUserActive) {
95
- activeSeconds++;
96
- }
97
- if (activeSeconds >= 60) {
98
- sendHeartbeat();
99
- activeSeconds = 0;
100
- }
101
- }, 1000);
102
-
103
- function sendHeartbeat() {
104
- $.post('/api/niki-loyalty/heartbeat', { _csrf: config.csrf_token }, function(res) {
105
- if (res.earned) {
106
- // Puanı güncelle
107
- $('#niki-live-points').text(res.total);
108
- // Hafızayı da güncelle
109
- localStorage.setItem('niki_last_points', res.total);
110
-
111
- showNikiToast(`+${res.points} Puan Kazandın! ☕`);
112
- $('#niki-floating-widget').addClass('niki-bounce');
113
- setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
134
+ $.get('/api/niki-loyalty/kasa-history', function (rows) {
135
+ tbody.empty();
136
+ if (!rows || rows.length === 0) {
137
+ tbody.append('<tr><td colspan="5" class="text-center">Geçmiş işlem yok.</td></tr>');
138
+ return;
114
139
  }
140
+ rows.forEach(r => {
141
+ const dateStr = new Date(r.ts).toLocaleDateString() + ' ' + new Date(r.ts).toLocaleTimeString();
142
+ const rowHtml = `
143
+ <tr>
144
+ <td>${dateStr}</td>
145
+ <td>
146
+ <a href="${r.profileUrl}" target="_blank" class="text-decoration-none">
147
+ <span class="avatar avatar-sm" style="background-color: ${r.iconBg};">${r.cust.charAt(0).toUpperCase()}</span>
148
+ ${r.cust}
149
+ </a>
150
+ </td>
151
+ <td>${r.reward}</td>
152
+ <td class="text-danger">-${formatPoints(r.amt)}</td>
153
+ </tr>
154
+ `;
155
+ tbody.append(rowHtml);
156
+ });
115
157
  });
116
158
  }
117
159
 
118
- function showNikiToast(msg) {
119
- $('.niki-toast').remove();
120
- const toast = $(`<div class="niki-toast"><i class="fa fa-paw"></i> ${msg}</div>`);
121
- $('body').append(toast);
122
- setTimeout(() => { toast.addClass('show'); }, 100);
123
- setTimeout(() => {
124
- toast.removeClass('show');
125
- setTimeout(() => toast.remove(), 3000);
126
- }, 3000);
160
+ function setupKasaScanner() {
161
+ $('#form-scan-qr').off('submit').on('submit', function (e) {
162
+ e.preventDefault();
163
+ const token = $('#qr-input').val().trim();
164
+ if (!token) return;
165
+
166
+ $.post('/api/niki-loyalty/scan-qr', { token: token }, function (res) {
167
+ if (res.success) {
168
+ app.alert({
169
+ type: 'success',
170
+ title: 'İşlem Başarılı! ✅',
171
+ message: `
172
+ <strong>Müşteri:</strong> ${res.customer.username}<br>
173
+ <strong>Verilen:</strong> ${res.rewardName}<br>
174
+ <strong>Tutar:</strong> ${res.cost} Puan
175
+ `,
176
+ timeout: 5000
177
+ });
178
+ $('#qr-input').val(''); // Inputu temizle
179
+ loadKasaHistory(); // Tabloyu güncelle
180
+ } else {
181
+ app.alert({ type: 'danger', title: 'Hata', message: res.message });
182
+ }
183
+ });
184
+ });
127
185
  }
128
186
  });