diff --git a/src/admin/routes/dashboard.js b/src/admin/routes/dashboard.js index 2a3aaff..a09a471 100644 --- a/src/admin/routes/dashboard.js +++ b/src/admin/routes/dashboard.js @@ -94,6 +94,13 @@ router.get('/', async (req, res) => { ); const revenueData30 = fillDays(days30, revenueRows30, 'day', 'revenue'); + const userRows7 = await db.allAsync( + `SELECT date(created_at) as day, COUNT(*) as count + FROM users WHERE created_at >= date('now', '-7 days') + GROUP BY day ORDER BY day` + ); + const usersData7 = fillDays(days7, userRows7, 'day', 'count'); + const topProducts = await db.allAsync( `SELECT p.name, SUM(pu.quantity) as qty, COALESCE(SUM(pu.total_price), 0) as revenue FROM purchases pu JOIN products p ON pu.product_id = p.id @@ -149,10 +156,11 @@ router.get('/', async (req, res) => { }, message, chartData: { - days7, + days: days7, days30, - revenueData7, + revenueData: revenueData7, revenueData30, + usersData: usersData7, topProducts, topSpenders, revenueByCategory, diff --git a/src/admin/routes/seed.js b/src/admin/routes/seed.js index 34e8068..75e97e6 100644 --- a/src/admin/routes/seed.js +++ b/src/admin/routes/seed.js @@ -8,16 +8,32 @@ router.get('/', (req, res) => { res.render('seed', { title: 'Seed & Reset' }); }); +function dateStr(daysAgo, hours, minutes) { + const d = new Date(); + d.setDate(d.getDate() - daysAgo); + d.setHours(hours || 0, minutes || 0, 0, 0); + return d.toISOString().replace('T', ' ').substring(0, 19); +} + +function randInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function pick(arr) { + return arr[Math.floor(Math.random() * arr.length)]; +} + // Require re-auth (re-enter admin token) before destructive actions router.post('/seed-demo', requireReAuth, async (req, res) => { try { const delTables = ['purchases', 'transactions', 'crypto_wallets', 'audit_log', - 'user_states', 'products', 'subcategories', 'categories', 'users', 'locations']; + 'user_states', 'products', 'subcategories', 'categories', 'users', 'locations', 'commission_payments']; for (const t of delTables) { await db.runAsync(`DELETE FROM ${t}`); } await db.runAsync("DELETE FROM sqlite_sequence WHERE name != '_meta'"); + // ── Locations ────────────────────────────────────────────── await db.runAsync(`INSERT INTO locations (country, city, district) VALUES ('Russia', 'Moscow', 'Center'), ('Russia', 'Saint Petersburg', 'North'), @@ -27,6 +43,7 @@ router.post('/seed-demo', requireReAuth, async (req, res) => { const locSPb = locs.find(l => l.city === 'Saint Petersburg').id; const locBerlin = locs.find(l => l.city === 'Berlin').id; + // ── Categories ───────────────────────────────────────────── await db.runAsync('INSERT INTO categories (location_id, name) VALUES (?, ?), (?, ?), (?, ?), (?, ?), (?, ?)', [locMoscow, 'Digital', locMoscow, 'Physical', locSPb, 'Premium', locSPb, 'VIP', locBerlin, 'Standard']); const cats = await db.allAsync('SELECT id, name FROM categories'); @@ -36,6 +53,7 @@ router.post('/seed-demo', requireReAuth, async (req, res) => { const catVIP = cats.find(c => c.name === 'VIP').id; const catStandard = cats.find(c => c.name === 'Standard').id; + // ── Subcategories ────────────────────────────────────────── await db.runAsync(`INSERT INTO subcategories (category_id, name) VALUES (?, 'VPN'), (?, 'Accounts'), (?, 'Software'), (?, 'Hardware'), (?, 'Accessories'), @@ -59,18 +77,32 @@ router.post('/seed-demo', requireReAuth, async (req, res) => { const subStarter = subs.find(s => s.name === 'Starter' && s.category_id === catStandard).id; const subSoftware = subs.find(s => s.name === 'Software' && s.category_id === catDigital).id; - await db.runAsync(`INSERT INTO users (telegram_id, username, country, city, district, status, total_balance, bonus_balance) VALUES - ('1001', 'alice', 'Russia', 'Moscow', 'Center', 0, 150.00, 25.00), - ('1002', 'bob', 'Russia', 'Moscow', 'Center', 0, 85.50, 10.00), - ('1003', 'charlie', 'Russia', 'Saint Petersburg', 'North', 0, 320.75, 50.00), - ('1004', 'diana', 'Germany', 'Berlin', 'Mitte', 0, 45.00, 5.00), - ('1005', 'evan', 'Germany', 'Berlin', 'Mitte', 0, 0.00, 0.00)`); - const users = await db.allAsync('SELECT id, username FROM users'); - const uAlice = users.find(u => u.username === 'alice').id; - const uBob = users.find(u => u.username === 'bob').id; - const uCharlie = users.find(u => u.username === 'charlie').id; - const uDiana = users.find(u => u.username === 'diana').id; + // ── Users (created_at spread across last 30 days) ────────── + const userDefs = [ + { tg: '1001', name: 'alice', country: 'Russia', city: 'Moscow', district: 'Center', bal: 150.00, bonus: 25.00, daysAgo: 28 }, + { tg: '1002', name: 'bob', country: 'Russia', city: 'Moscow', district: 'Center', bal: 85.50, bonus: 10.00, daysAgo: 25 }, + { tg: '1003', name: 'charlie', country: 'Russia', city: 'Saint Petersburg', district: 'North', bal: 320.75, bonus: 50.00, daysAgo: 22 }, + { tg: '1004', name: 'diana', country: 'Germany', city: 'Berlin', district: 'Mitte', bal: 45.00, bonus: 5.00, daysAgo: 18 }, + { tg: '1005', name: 'evan', country: 'Germany', city: 'Berlin', district: 'Mitte', bal: 0.00, bonus: 0.00, daysAgo: 15 }, + { tg: '1006', name: 'frank', country: 'Russia', city: 'Moscow', district: 'Center', bal: 210.00, bonus: 30.00, daysAgo: 12 }, + { tg: '1007', name: 'grace', country: 'Russia', city: 'Saint Petersburg', district: 'North', bal: 75.25, bonus: 15.00, daysAgo: 8 }, + { tg: '1008', name: 'henry', country: 'Germany', city: 'Berlin', district: 'Mitte', bal: 500.00, bonus: 100.00, daysAgo: 5 }, + { tg: '1009', name: 'iris', country: 'Russia', city: 'Moscow', district: 'Center', bal: 0.00, bonus: 0.00, daysAgo: 3 }, + { tg: '1010', name: 'jack', country: 'Germany', city: 'Berlin', district: 'Mitte', bal: 33.00, bonus: 5.00, daysAgo: 1 }, + ]; + for (const u of userDefs) { + await db.runAsync( + `INSERT INTO users (telegram_id, username, country, city, district, status, total_balance, bonus_balance, created_at) + VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?)`, + [u.tg, u.name, u.country, u.city, u.district, u.bal, u.bonus, dateStr(u.daysAgo, randInt(8, 20), randInt(0, 59))] + ); + } + const users = await db.allAsync('SELECT id, username FROM users'); + const uid = {}; + for (const u of users) uid[u.username] = u.id; + + // ── Products ─────────────────────────────────────────────── await db.runAsync(`INSERT INTO products (location_id, category_id, subcategory_id, name, description, price, quantity_in_stock, photo_url) VALUES (?, ?, ?, 'VPN Subscription 30d', 'Premium VPN access for 30 days', 9.99, 100, ''), (?, ?, ?, 'VPN Subscription 90d', 'Premium VPN access for 90 days', 24.99, 50, ''), @@ -87,34 +119,85 @@ router.post('/seed-demo', requireReAuth, async (req, res) => { locBerlin, catStandard, subBasic, locMoscow, catDigital, subSoftware, locSPb, catVIP, subExpress, locBerlin, catStandard, subStarter]); - const prods = await db.allAsync('SELECT id, name FROM products'); - const pVPN30 = prods.find(p => p.name.includes('30d')).id; - const pUSB = prods.find(p => p.name.includes('USB')).id; - const pPrem1y = prods.find(p => p.name.includes('1 Year')).id; - const pStd = prods.find(p => p.name.includes('Standard')).id; - const pStarter = prods.find(p => p.name.includes('Starter')).id; + const prods = await db.allAsync('SELECT id, name, price FROM products'); - await db.runAsync(`INSERT INTO crypto_wallets (user_id, wallet_type, address, derivation_path, mnemonic, balance) VALUES - (?, 'BTC', 'bc1qexample1addr', 'm/44h/0h/0h/0/0', 'encrypted:1', 0.00543), - (?, 'ETH', '0xExampleEth1addr', 'm/44h/60h/0h/0/0', 'encrypted:2', 0.12500), - (?, 'BTC', 'bc1qexample2addr', 'm/44h/0h/0h/0/1', 'encrypted:3', 0.00210), - (?, 'LTC', 'ltc1qexample3addr', 'm/44h/2h/0h/0/0', 'encrypted:4', 1.50000), - (?, 'ETH', '0xExampleEth4addr', 'm/44h/60h/0h/0/1', 'encrypted:5', 0.50000)`, - [uAlice, uAlice, uBob, uCharlie, uDiana]); + // ── Purchases (25-30, spread across last 30 days) ────────── + const walletTypes = ['BTC', 'ETH', 'LTC', 'USDT']; + const userList = Object.values(uid); + const count = randInt(25, 30); - await db.runAsync(`INSERT INTO purchases (user_id, product_id, wallet_type, tx_hash, quantity, total_price, status) VALUES - (?, ?, 'BTC', 'tx_a1b2c3d4', 1, 9.99, 'completed'), - (?, ?, 'ETH', 'tx_b2c3d4e5', 2, 59.98, 'completed'), - (?, ?, 'LTC', 'tx_c3d4e5f6', 1, 99.99, 'pending'), - (?, ?, 'BTC', 'tx_d4e5f6a7', 1, 14.99, 'completed'), - (?, ?, 'ETH', 'tx_e5f6a7b8', 3, 14.97, 'cancelled')`, - [uAlice, pVPN30, uBob, pUSB, uCharlie, pPrem1y, uAlice, pStd, uDiana, pStarter]); + for (let i = 0; i < count; i++) { + const daysAgo = randInt(0, 29); + const product = pick(prods); + const user = pick(userList); + const qty = randInt(1, 5); + const totalPrice = (product.price * qty).toFixed(2); + const roll = Math.random(); + const status = roll < 0.70 ? 'completed' : (roll < 0.90 ? 'pending' : 'cancelled'); + const txHash = 'tx_' + Math.random().toString(36).substring(2, 10); + const wallet = pick(walletTypes); + await db.runAsync( + `INSERT INTO purchases (user_id, product_id, wallet_type, tx_hash, quantity, total_price, status, purchase_date) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [user, product.id, wallet, txHash, qty, totalPrice, status, dateStr(daysAgo, randInt(0, 23), randInt(0, 59))] + ); + } + + // ── Crypto Wallets (10 total, diverse coins) ─────────────── + const walletDefs = [ + { user: 'alice', type: 'BTC', addr: 'bc1qexample1addr', path: 'm/44h/0h/0h/0/0', mnem: 'encrypted:1', bal: 0.00543 }, + { user: 'alice', type: 'ETH', addr: '0xExampleEth1addr', path: 'm/44h/60h/0h/0/0', mnem: 'encrypted:2', bal: 0.12500 }, + { user: 'bob', type: 'BTC', addr: 'bc1qexample2addr', path: 'm/44h/0h/0h/0/1', mnem: 'encrypted:3', bal: 0.00210 }, + { user: 'charlie', type: 'LTC', addr: 'ltc1qexample3addr', path: 'm/44h/2h/0h/0/0', mnem: 'encrypted:4', bal: 1.50000 }, + { user: 'diana', type: 'ETH', addr: '0xExampleEth4addr', path: 'm/44h/60h/0h/0/1', mnem: 'encrypted:5', bal: 0.50000 }, + { user: 'frank', type: 'XRP', addr: 'rExampleXRP1addr', path: 'm/44h/144h/0h/0/0', mnem: 'encrypted:6', bal: 250.00000 }, + { user: 'grace', type: 'BCH', addr: 'bitcoincash:qexample1', path: 'm/44h/145h/0h/0/0', mnem: 'encrypted:7', bal: 0.75000 }, + { user: 'henry', type: 'DOGE', addr: 'DExampleDoge1addr', path: 'm/44h/3h/0h/0/0', mnem: 'encrypted:8', bal: 5000.00000 }, + { user: 'iris', type: 'BTC', addr: 'bc1qexample9addr', path: 'm/44h/0h/0h/0/2', mnem: 'encrypted:9', bal: 0.00000 }, + { user: 'jack', type: 'USDT', addr: '0xExampleUSDTaddr', path: 'm/44h/60h/0h/0/2', mnem: 'encrypted:10', bal: 0.00000 }, + ]; + + for (const w of walletDefs) { + await db.runAsync( + `INSERT INTO crypto_wallets (user_id, wallet_type, address, derivation_path, mnemonic, balance) + VALUES (?, ?, ?, ?, ?, ?)`, + [uid[w.user], w.type, w.addr, w.path, w.mnem, w.bal] + ); + } + + // ── Transactions ─────────────────────────────────────────── await db.runAsync(`INSERT INTO transactions (user_id, wallet_type, tx_hash, amount) VALUES (?, 'BTC', 'tx_f6a7b8c9', 0.01000), (?, 'ETH', 'tx_a7b8c9d0', 0.50000), - (?, 'LTC', 'tx_b8c9d0e1', 2.00000)`, - [uAlice, uBob, uCharlie]); + (?, 'LTC', 'tx_b8c9d0e1', 2.00000), + (?, 'XRP', 'tx_xrp1a2b3', 100.00000), + (?, 'DOGE', 'tx_doge1c2d', 1000.00000)`, + [uid.alice, uid.bob, uid.charlie, uid.frank, uid.henry]); + + // ── Audit Log (10+ entries, spread across last 7 days) ───── + await db.runAsync(`INSERT INTO audit_log (action, admin_id, details, created_at) VALUES + ('login', 'admin', 'Admin logged in', ?), + ('product_created', 'admin', 'Created VPN Subscription', ?), + ('user_blocked', 'admin', 'Blocked user bob', ?), + ('settings_changed', 'system', 'Commission rate updated to 5%', ?), + ('purchase_approved', 'auto', 'Auto-approved pending purchase #3', ?), + ('wallet_added', 'admin', 'Added BTC wallet for alice', ?), + ('category_created', 'admin', 'Created Premium category', ?), + ('product_updated', 'admin', 'Updated USB Drive stock', ?), + ('commission_paid', 'admin', 'Paid commission $50', ?), + ('user_registered', 'auto', 'New user evan registered', ?), + ('login', 'admin', 'Admin logged in from new IP', ?), + ('settings_changed', 'system', 'Maintenance mode disabled', ?)`, + [dateStr(1, 9, 15), dateStr(2, 11, 30), dateStr(3, 14, 0), dateStr(4, 8, 45), + dateStr(5, 16, 20), dateStr(6, 10, 0), dateStr(7, 13, 10), dateStr(1, 17, 55), + dateStr(10, 12, 0), dateStr(8, 9, 30), dateStr(0, 7, 0), dateStr(2, 22, 15)]); + + // ── Commission Payments ───────────────────────────────────── + await db.runAsync(`INSERT INTO commission_payments (total_balance_usd, commission_rate, commission_amount_usd, paid_amount_usd, wallet_count, note, created_at) VALUES + (1250.00, 5.0, 62.50, 50.00, 8, 'Monthly commission payment', ?), + (980.00, 5.0, 49.00, 25.50, 6, 'Partial commission', ?)`, + [dateStr(10, 12, 0), dateStr(20, 15, 30)]); res.redirect('/?seeded=1'); } catch (err) { @@ -126,7 +209,7 @@ router.post('/seed-demo', requireReAuth, async (req, res) => { router.post('/clear-all', requireReAuth, async (req, res) => { try { const tables = ['purchases', 'transactions', 'crypto_wallets', 'audit_log', - 'user_states', 'products', 'subcategories', 'categories', 'users', 'locations']; + 'user_states', 'products', 'subcategories', 'categories', 'users', 'locations', 'commission_payments']; for (const t of tables) { await db.runAsync(`DELETE FROM ${t}`); } diff --git a/src/admin/views/dashboard.ejs b/src/admin/views/dashboard.ejs index 2bffc8a..2f57a20 100644 --- a/src/admin/views/dashboard.ejs +++ b/src/admin/views/dashboard.ejs @@ -98,7 +98,7 @@
-
+
@@ -114,7 +114,7 @@
-
+
@@ -126,7 +126,7 @@
-
+
@@ -337,10 +337,10 @@ diff --git a/src/admin/views/layout.ejs b/src/admin/views/layout.ejs index c2d5a92..ba9787b 100644 --- a/src/admin/views/layout.ejs +++ b/src/admin/views/layout.ejs @@ -76,9 +76,12 @@ gap: 0.4rem; } /* Draggable panel cursor */ - .panel-hdr { cursor: grab; } - .panel-hdr:active { cursor: grabbing; } - .sortable-ghost { opacity: 0.4; } + .apex-charts { + min-height: 200px; + } + .apex-charts.sparkline { + min-height: 50px; + } diff --git a/src/admin/views/partials/app-head-css.ejs b/src/admin/views/partials/app-head-css.ejs index b7386cf..d3034d2 100644 --- a/src/admin/views/partials/app-head-css.ejs +++ b/src/admin/views/partials/app-head-css.ejs @@ -8,5 +8,8 @@ + + + \ No newline at end of file diff --git a/src/admin/views/partials/app-scripts.ejs b/src/admin/views/partials/app-scripts.ejs index ac2e8b5..dc2cf6f 100644 --- a/src/admin/views/partials/app-scripts.ejs +++ b/src/admin/views/partials/app-scripts.ejs @@ -1,6 +1,5 @@ - @@ -10,27 +9,7 @@ - - - - - - - +