nodebb-plugin-niki-loyalty 1.0.28 → 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.
- package/library.js +193 -147
- package/package.json +1 -1
- package/static/lib/client.js +162 -104
package/library.js
CHANGED
|
@@ -2,56 +2,54 @@
|
|
|
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
|
-
//
|
|
12
|
+
// ⚙️ AYARLAR & KURALLAR (GAME LOGIC)
|
|
12
13
|
// =========================
|
|
13
14
|
const SETTINGS = {
|
|
14
|
-
|
|
15
|
-
dailyCap: 250,
|
|
15
|
+
dailyCap: 28, // Günlük Maksimum Puan
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
//
|
|
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
|
|
19
29
|
const REWARDS = [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
{ cost: 250, name: 'Ücretsiz Kahve ☕' },
|
|
31
|
+
{ cost: 180, name: '%60 İndirimli Kahve' },
|
|
32
|
+
{ cost: 120, name: '%30 İndirimli Kahve' },
|
|
33
|
+
{ cost: 60, name: '1 Kurabiye 🍪' },
|
|
24
34
|
];
|
|
25
35
|
|
|
26
|
-
// TEST MODE (Set to true to bypass point checks)
|
|
27
36
|
const TEST_MODE_UNLIMITED = false;
|
|
28
37
|
|
|
29
38
|
// =========================
|
|
30
|
-
//
|
|
39
|
+
// 🛠 YARDIMCI FONKSİYONLAR
|
|
31
40
|
// =========================
|
|
32
41
|
function safeParseMaybeJson(x) {
|
|
33
42
|
if (x == null) return null;
|
|
34
43
|
if (typeof x === 'object') return x;
|
|
35
|
-
|
|
36
|
-
try { return JSON.parse(x); } catch (e) { return null; }
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
44
|
+
try { return JSON.parse(x); } catch (e) { return null; }
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
function safeStringify(obj) {
|
|
42
48
|
try { return JSON.stringify(obj); } catch (e) { return null; }
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
// =========================
|
|
46
|
-
// LOGGING
|
|
47
|
-
// =========================
|
|
48
51
|
async function addUserLog(uid, type, amount, desc) {
|
|
49
|
-
const logEntry = {
|
|
50
|
-
ts: Date.now(),
|
|
51
|
-
type, // 'earn' | 'spend'
|
|
52
|
-
amt: amount,
|
|
53
|
-
txt: desc,
|
|
54
|
-
};
|
|
52
|
+
const logEntry = { ts: Date.now(), type, amt: amount, txt: desc };
|
|
55
53
|
const payload = safeStringify(logEntry);
|
|
56
54
|
if (!payload) return;
|
|
57
55
|
await db.listAppend(`niki:activity:${uid}`, payload);
|
|
@@ -59,13 +57,8 @@ async function addUserLog(uid, type, amount, desc) {
|
|
|
59
57
|
}
|
|
60
58
|
|
|
61
59
|
async function addKasaLog(staffUid, customerName, customerUid, rewardName, amount) {
|
|
62
|
-
const logEntry = {
|
|
63
|
-
ts: Date.now(),
|
|
64
|
-
staff: staffUid,
|
|
65
|
-
cust: customerName,
|
|
66
|
-
cuid: customerUid,
|
|
67
|
-
amt: amount,
|
|
68
|
-
reward: rewardName // Store the specific reward name
|
|
60
|
+
const logEntry = {
|
|
61
|
+
ts: Date.now(), staff: staffUid, cust: customerName, cuid: customerUid, amt: amount, reward: rewardName
|
|
69
62
|
};
|
|
70
63
|
const payload = safeStringify(logEntry);
|
|
71
64
|
if (!payload) return;
|
|
@@ -73,74 +66,152 @@ async function addKasaLog(staffUid, customerName, customerUid, rewardName, amoun
|
|
|
73
66
|
await db.listTrim('niki:kasa:history', -100, -1);
|
|
74
67
|
}
|
|
75
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
|
+
|
|
76
159
|
// =========================
|
|
77
|
-
//
|
|
160
|
+
// 🚀 INIT & ROUTES
|
|
78
161
|
// =========================
|
|
79
162
|
Plugin.init = async function (params) {
|
|
80
163
|
const router = params.router;
|
|
81
164
|
const middleware = params.middleware;
|
|
82
165
|
|
|
83
|
-
// 1) HEARTBEAT (
|
|
166
|
+
// 1) HEARTBEAT (Artık "Okuma" Puanı veriyor)
|
|
167
|
+
// Client-side script her 30-60 saniyede bir bu adrese istek atmalıdır.
|
|
84
168
|
router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
|
|
85
169
|
try {
|
|
86
170
|
const uid = req.uid;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const currentDailyScore = parseInt((await db.getObjectField(dailyKey, 'score')) || 0, 10);
|
|
91
|
-
|
|
92
|
-
if (currentDailyScore >= SETTINGS.dailyCap) {
|
|
93
|
-
return res.json({ earned: false, reason: 'daily_cap' });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
|
|
97
|
-
await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
|
|
98
|
-
|
|
171
|
+
// Heartbeat geldiğinde "read" aksiyonunu tetikle
|
|
172
|
+
await awardDailyAction(uid, 'read');
|
|
173
|
+
|
|
99
174
|
const newBalance = await user.getUserField(uid, 'niki_points');
|
|
100
|
-
return res.json({ earned: true,
|
|
175
|
+
return res.json({ earned: true, total: newBalance });
|
|
101
176
|
} catch (err) {
|
|
102
|
-
return res.status(500).json({
|
|
177
|
+
return res.status(500).json({ error: 'error' });
|
|
103
178
|
}
|
|
104
179
|
});
|
|
105
180
|
|
|
106
|
-
// 2) WALLET DATA (
|
|
181
|
+
// 2) WALLET DATA (Cüzdan Bilgileri)
|
|
107
182
|
router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
|
|
108
183
|
try {
|
|
109
184
|
const uid = req.uid;
|
|
110
185
|
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
111
|
-
|
|
186
|
+
|
|
112
187
|
const [userData, dailyData, historyRaw] = await Promise.all([
|
|
113
188
|
user.getUserFields(uid, ['niki_points']),
|
|
114
189
|
db.getObject(`niki:daily:${uid}:${today}`),
|
|
115
190
|
db.getListRange(`niki:activity:${uid}`, 0, -1),
|
|
116
191
|
]);
|
|
117
192
|
|
|
118
|
-
const
|
|
119
|
-
const dailyScore = parseInt(dailyData?.score || 0, 10);
|
|
120
|
-
|
|
193
|
+
const dailyScore = parseFloat(dailyData?.score || 0);
|
|
121
194
|
let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
|
|
122
195
|
if (dailyPercent > 100) dailyPercent = 100;
|
|
123
196
|
|
|
124
|
-
const history = (historyRaw || [])
|
|
125
|
-
.map(safeParseMaybeJson)
|
|
126
|
-
.filter(Boolean)
|
|
127
|
-
.reverse();
|
|
197
|
+
const history = (historyRaw || []).map(safeParseMaybeJson).filter(Boolean).reverse();
|
|
128
198
|
|
|
129
199
|
return res.json({
|
|
130
|
-
points:
|
|
200
|
+
points: parseInt(userData?.niki_points || 0, 10),
|
|
131
201
|
dailyScore,
|
|
132
202
|
dailyCap: SETTINGS.dailyCap,
|
|
133
203
|
dailyPercent,
|
|
134
204
|
history,
|
|
135
|
-
rewards: REWARDS
|
|
205
|
+
rewards: REWARDS,
|
|
136
206
|
});
|
|
137
207
|
} catch (err) {
|
|
138
208
|
return res.status(500).json({ points: 0, history: [] });
|
|
139
209
|
}
|
|
140
210
|
});
|
|
141
211
|
|
|
142
|
-
// 3) KASA HISTORY
|
|
212
|
+
// 3) KASA HISTORY
|
|
143
213
|
router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
|
|
214
|
+
// ... (Mevcut kodunun aynısı - sadece yetki kontrolü var)
|
|
144
215
|
try {
|
|
145
216
|
const isAdmin = await user.isAdministrator(req.uid);
|
|
146
217
|
const isMod = await user.isGlobalModerator(req.uid);
|
|
@@ -148,116 +219,85 @@ Plugin.init = async function (params) {
|
|
|
148
219
|
|
|
149
220
|
const raw = await db.getListRange('niki:kasa:history', 0, -1);
|
|
150
221
|
const rows = (raw || []).map(safeParseMaybeJson).filter(Boolean).reverse();
|
|
151
|
-
|
|
222
|
+
|
|
223
|
+
// Kullanıcı detaylarını doldurma (Map logic)
|
|
152
224
|
const uids = rows.map(r => parseInt(r.cuid, 10)).filter(n => Number.isFinite(n) && n > 0);
|
|
153
225
|
const users = await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'icon:bgColor']);
|
|
154
226
|
const userMap = {};
|
|
155
|
-
(users || []).forEach(u =>
|
|
227
|
+
(users || []).forEach(u => userMap[u.uid] = u);
|
|
156
228
|
|
|
157
229
|
const rp = nconf.get('relative_path') || '';
|
|
158
|
-
|
|
159
230
|
const enriched = rows.map(r => {
|
|
160
|
-
const
|
|
161
|
-
const u = userMap[uid];
|
|
162
|
-
if (!u) return r;
|
|
231
|
+
const u = userMap[r.cuid] || {};
|
|
163
232
|
return {
|
|
164
233
|
...r,
|
|
165
234
|
cust: u.username || r.cust || 'Bilinmeyen',
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
profileUrl:
|
|
235
|
+
picture: u.picture || '',
|
|
236
|
+
iconBg: u['icon:bgColor'] || '#4b5563',
|
|
237
|
+
profileUrl: u.userslug ? `${rp}/user/${u.userslug}` : '',
|
|
238
|
+
reward: r.reward || 'İşlem'
|
|
169
239
|
};
|
|
170
240
|
});
|
|
171
|
-
|
|
172
241
|
return res.json(enriched);
|
|
173
|
-
} catch (
|
|
174
|
-
return res.status(500).json([]);
|
|
175
|
-
}
|
|
242
|
+
} catch(e) { return res.status(500).json([]); }
|
|
176
243
|
});
|
|
177
244
|
|
|
178
|
-
// 4)
|
|
245
|
+
// 4) QR OLUŞTURMA
|
|
179
246
|
router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
|
|
180
247
|
try {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
await db.expire(`niki:qr:${token}`, 120);
|
|
194
|
-
|
|
195
|
-
return res.json({ success: true, token });
|
|
196
|
-
} catch (err) {
|
|
197
|
-
return res.status(500).json({ success: false, message: 'Sunucu hatası' });
|
|
198
|
-
}
|
|
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
|
|
251
|
+
|
|
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 }); }
|
|
199
260
|
});
|
|
200
261
|
|
|
201
|
-
// 5)
|
|
262
|
+
// 5) QR TARATMA (Kasa İşlemi)
|
|
202
263
|
router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
await db.delete(`niki:qr:${token}`);
|
|
237
|
-
|
|
238
|
-
const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
|
|
239
|
-
|
|
240
|
-
// Logs
|
|
241
|
-
await addUserLog(customerUid, 'spend', selectedReward.cost, selectedReward.name);
|
|
242
|
-
await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid, selectedReward.name, selectedReward.cost);
|
|
243
|
-
|
|
244
|
-
return res.json({
|
|
245
|
-
success: true,
|
|
246
|
-
customer: customerData,
|
|
247
|
-
rewardName: selectedReward.name,
|
|
248
|
-
cost: selectedReward.cost,
|
|
249
|
-
message: 'Onaylandı!',
|
|
250
|
-
});
|
|
251
|
-
} catch (err) {
|
|
252
|
-
return res.status(500).json({ success: false, message: 'Sunucu hatası' });
|
|
253
|
-
}
|
|
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);
|
|
286
|
+
}
|
|
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}); }
|
|
254
295
|
});
|
|
255
296
|
|
|
256
|
-
// 6)
|
|
297
|
+
// 6) SAYFA ROTALARI
|
|
257
298
|
routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
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', {});
|
|
261
301
|
return res.render('niki-kasa', { title: 'Niki Kasa' });
|
|
262
302
|
});
|
|
263
303
|
};
|
|
@@ -268,7 +308,13 @@ Plugin.addScripts = async function (scripts) {
|
|
|
268
308
|
};
|
|
269
309
|
|
|
270
310
|
Plugin.addNavigation = async function (nav) {
|
|
271
|
-
nav.push({
|
|
311
|
+
nav.push({
|
|
312
|
+
route: '/niki-wallet',
|
|
313
|
+
title: 'Niki Cüzdan',
|
|
314
|
+
enabled: true,
|
|
315
|
+
iconClass: 'fa-coffee',
|
|
316
|
+
text: 'Niki Cüzdan',
|
|
317
|
+
});
|
|
272
318
|
return nav;
|
|
273
319
|
};
|
|
274
320
|
|
package/package.json
CHANGED
package/static/lib/client.js
CHANGED
|
@@ -1,128 +1,186 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
/* globals $, app, socket, ajaxify, utils */
|
|
4
|
+
|
|
3
5
|
$(document).ready(function () {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
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
|
-
//
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
$('#
|
|
48
|
-
$('#
|
|
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
|
-
//
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
119
|
-
$('
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
});
|