fix(admin/dashboard): charts rendering + realistic demo data
- Move ApexCharts script to <head> in app-head-css.ejs for global availability
- Remove conflicting apexchartsWrapper.js module from app-scripts.ejs
- dashboard.ejs: wrap charts init in window.addEventListener('load') for correct DOM timing
- Add inline height styles to chart containers (350px, 300px, 250px)
- Add CSS min-height fallback for .apex-charts in layout.ejs
- seed.js: realistic demo data with 30-day date spread, 10 users, 30 purchases,
12 audit log entries, 2 commission payments, 10 wallets with 7 coin types
- dashboard.js: add days30 and revenueData30 to chartData for 30-day chart
This commit is contained in:
@@ -94,6 +94,13 @@ router.get('/', async (req, res) => {
|
|||||||
);
|
);
|
||||||
const revenueData30 = fillDays(days30, revenueRows30, 'day', 'revenue');
|
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(
|
const topProducts = await db.allAsync(
|
||||||
`SELECT p.name, SUM(pu.quantity) as qty, COALESCE(SUM(pu.total_price), 0) as revenue
|
`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
|
FROM purchases pu JOIN products p ON pu.product_id = p.id
|
||||||
@@ -149,10 +156,11 @@ router.get('/', async (req, res) => {
|
|||||||
},
|
},
|
||||||
message,
|
message,
|
||||||
chartData: {
|
chartData: {
|
||||||
days7,
|
days: days7,
|
||||||
days30,
|
days30,
|
||||||
revenueData7,
|
revenueData: revenueData7,
|
||||||
revenueData30,
|
revenueData30,
|
||||||
|
usersData: usersData7,
|
||||||
topProducts,
|
topProducts,
|
||||||
topSpenders,
|
topSpenders,
|
||||||
revenueByCategory,
|
revenueByCategory,
|
||||||
|
|||||||
@@ -8,16 +8,32 @@ router.get('/', (req, res) => {
|
|||||||
res.render('seed', { title: 'Seed & Reset' });
|
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
|
// Require re-auth (re-enter admin token) before destructive actions
|
||||||
router.post('/seed-demo', requireReAuth, async (req, res) => {
|
router.post('/seed-demo', requireReAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const delTables = ['purchases', 'transactions', 'crypto_wallets', 'audit_log',
|
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) {
|
for (const t of delTables) {
|
||||||
await db.runAsync(`DELETE FROM ${t}`);
|
await db.runAsync(`DELETE FROM ${t}`);
|
||||||
}
|
}
|
||||||
await db.runAsync("DELETE FROM sqlite_sequence WHERE name != '_meta'");
|
await db.runAsync("DELETE FROM sqlite_sequence WHERE name != '_meta'");
|
||||||
|
|
||||||
|
// ── Locations ──────────────────────────────────────────────
|
||||||
await db.runAsync(`INSERT INTO locations (country, city, district) VALUES
|
await db.runAsync(`INSERT INTO locations (country, city, district) VALUES
|
||||||
('Russia', 'Moscow', 'Center'),
|
('Russia', 'Moscow', 'Center'),
|
||||||
('Russia', 'Saint Petersburg', 'North'),
|
('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 locSPb = locs.find(l => l.city === 'Saint Petersburg').id;
|
||||||
const locBerlin = locs.find(l => l.city === 'Berlin').id;
|
const locBerlin = locs.find(l => l.city === 'Berlin').id;
|
||||||
|
|
||||||
|
// ── Categories ─────────────────────────────────────────────
|
||||||
await db.runAsync('INSERT INTO categories (location_id, name) VALUES (?, ?), (?, ?), (?, ?), (?, ?), (?, ?)',
|
await db.runAsync('INSERT INTO categories (location_id, name) VALUES (?, ?), (?, ?), (?, ?), (?, ?), (?, ?)',
|
||||||
[locMoscow, 'Digital', locMoscow, 'Physical', locSPb, 'Premium', locSPb, 'VIP', locBerlin, 'Standard']);
|
[locMoscow, 'Digital', locMoscow, 'Physical', locSPb, 'Premium', locSPb, 'VIP', locBerlin, 'Standard']);
|
||||||
const cats = await db.allAsync('SELECT id, name FROM categories');
|
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 catVIP = cats.find(c => c.name === 'VIP').id;
|
||||||
const catStandard = cats.find(c => c.name === 'Standard').id;
|
const catStandard = cats.find(c => c.name === 'Standard').id;
|
||||||
|
|
||||||
|
// ── Subcategories ──────────────────────────────────────────
|
||||||
await db.runAsync(`INSERT INTO subcategories (category_id, name) VALUES
|
await db.runAsync(`INSERT INTO subcategories (category_id, name) VALUES
|
||||||
(?, 'VPN'), (?, 'Accounts'), (?, 'Software'),
|
(?, 'VPN'), (?, 'Accounts'), (?, 'Software'),
|
||||||
(?, 'Hardware'), (?, 'Accessories'),
|
(?, '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 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;
|
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
|
// ── Users (created_at spread across last 30 days) ──────────
|
||||||
('1001', 'alice', 'Russia', 'Moscow', 'Center', 0, 150.00, 25.00),
|
const userDefs = [
|
||||||
('1002', 'bob', 'Russia', 'Moscow', 'Center', 0, 85.50, 10.00),
|
{ tg: '1001', name: 'alice', country: 'Russia', city: 'Moscow', district: 'Center', bal: 150.00, bonus: 25.00, daysAgo: 28 },
|
||||||
('1003', 'charlie', 'Russia', 'Saint Petersburg', 'North', 0, 320.75, 50.00),
|
{ tg: '1002', name: 'bob', country: 'Russia', city: 'Moscow', district: 'Center', bal: 85.50, bonus: 10.00, daysAgo: 25 },
|
||||||
('1004', 'diana', 'Germany', 'Berlin', 'Mitte', 0, 45.00, 5.00),
|
{ tg: '1003', name: 'charlie', country: 'Russia', city: 'Saint Petersburg', district: 'North', bal: 320.75, bonus: 50.00, daysAgo: 22 },
|
||||||
('1005', 'evan', 'Germany', 'Berlin', 'Mitte', 0, 0.00, 0.00)`);
|
{ tg: '1004', name: 'diana', country: 'Germany', city: 'Berlin', district: 'Mitte', bal: 45.00, bonus: 5.00, daysAgo: 18 },
|
||||||
const users = await db.allAsync('SELECT id, username FROM users');
|
{ tg: '1005', name: 'evan', country: 'Germany', city: 'Berlin', district: 'Mitte', bal: 0.00, bonus: 0.00, daysAgo: 15 },
|
||||||
const uAlice = users.find(u => u.username === 'alice').id;
|
{ tg: '1006', name: 'frank', country: 'Russia', city: 'Moscow', district: 'Center', bal: 210.00, bonus: 30.00, daysAgo: 12 },
|
||||||
const uBob = users.find(u => u.username === 'bob').id;
|
{ tg: '1007', name: 'grace', country: 'Russia', city: 'Saint Petersburg', district: 'North', bal: 75.25, bonus: 15.00, daysAgo: 8 },
|
||||||
const uCharlie = users.find(u => u.username === 'charlie').id;
|
{ tg: '1008', name: 'henry', country: 'Germany', city: 'Berlin', district: 'Mitte', bal: 500.00, bonus: 100.00, daysAgo: 5 },
|
||||||
const uDiana = users.find(u => u.username === 'diana').id;
|
{ 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
|
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 30d', 'Premium VPN access for 30 days', 9.99, 100, ''),
|
||||||
(?, ?, ?, 'VPN Subscription 90d', 'Premium VPN access for 90 days', 24.99, 50, ''),
|
(?, ?, ?, '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,
|
locBerlin, catStandard, subBasic, locMoscow, catDigital, subSoftware,
|
||||||
locSPb, catVIP, subExpress, locBerlin, catStandard, subStarter]);
|
locSPb, catVIP, subExpress, locBerlin, catStandard, subStarter]);
|
||||||
|
|
||||||
const prods = await db.allAsync('SELECT id, name FROM products');
|
const prods = await db.allAsync('SELECT id, name, price 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;
|
|
||||||
|
|
||||||
await db.runAsync(`INSERT INTO crypto_wallets (user_id, wallet_type, address, derivation_path, mnemonic, balance) VALUES
|
// ── Purchases (25-30, spread across last 30 days) ──────────
|
||||||
(?, 'BTC', 'bc1qexample1addr', 'm/44h/0h/0h/0/0', 'encrypted:1', 0.00543),
|
const walletTypes = ['BTC', 'ETH', 'LTC', 'USDT'];
|
||||||
(?, 'ETH', '0xExampleEth1addr', 'm/44h/60h/0h/0/0', 'encrypted:2', 0.12500),
|
const userList = Object.values(uid);
|
||||||
(?, 'BTC', 'bc1qexample2addr', 'm/44h/0h/0h/0/1', 'encrypted:3', 0.00210),
|
const count = randInt(25, 30);
|
||||||
(?, '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]);
|
|
||||||
|
|
||||||
await db.runAsync(`INSERT INTO purchases (user_id, product_id, wallet_type, tx_hash, quantity, total_price, status) VALUES
|
for (let i = 0; i < count; i++) {
|
||||||
(?, ?, 'BTC', 'tx_a1b2c3d4', 1, 9.99, 'completed'),
|
const daysAgo = randInt(0, 29);
|
||||||
(?, ?, 'ETH', 'tx_b2c3d4e5', 2, 59.98, 'completed'),
|
const product = pick(prods);
|
||||||
(?, ?, 'LTC', 'tx_c3d4e5f6', 1, 99.99, 'pending'),
|
const user = pick(userList);
|
||||||
(?, ?, 'BTC', 'tx_d4e5f6a7', 1, 14.99, 'completed'),
|
const qty = randInt(1, 5);
|
||||||
(?, ?, 'ETH', 'tx_e5f6a7b8', 3, 14.97, 'cancelled')`,
|
const totalPrice = (product.price * qty).toFixed(2);
|
||||||
[uAlice, pVPN30, uBob, pUSB, uCharlie, pPrem1y, uAlice, pStd, uDiana, pStarter]);
|
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
|
await db.runAsync(`INSERT INTO transactions (user_id, wallet_type, tx_hash, amount) VALUES
|
||||||
(?, 'BTC', 'tx_f6a7b8c9', 0.01000),
|
(?, 'BTC', 'tx_f6a7b8c9', 0.01000),
|
||||||
(?, 'ETH', 'tx_a7b8c9d0', 0.50000),
|
(?, 'ETH', 'tx_a7b8c9d0', 0.50000),
|
||||||
(?, 'LTC', 'tx_b8c9d0e1', 2.00000)`,
|
(?, 'LTC', 'tx_b8c9d0e1', 2.00000),
|
||||||
[uAlice, uBob, uCharlie]);
|
(?, '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');
|
res.redirect('/?seeded=1');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -126,7 +209,7 @@ router.post('/seed-demo', requireReAuth, async (req, res) => {
|
|||||||
router.post('/clear-all', requireReAuth, async (req, res) => {
|
router.post('/clear-all', requireReAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const tables = ['purchases', 'transactions', 'crypto_wallets', 'audit_log',
|
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) {
|
for (const t of tables) {
|
||||||
await db.runAsync(`DELETE FROM ${t}`);
|
await db.runAsync(`DELETE FROM ${t}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-container show">
|
<div class="panel-container show">
|
||||||
<div class="panel-content">
|
<div class="panel-content">
|
||||||
<div id="revenue30dChart" class="apex-charts"></div>
|
<div id="revenue30dChart" class="apex-charts" style="height:350px"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-container show">
|
<div class="panel-container show">
|
||||||
<div class="panel-content">
|
<div class="panel-content">
|
||||||
<div id="categoryChart" class="apex-charts"></div>
|
<div id="categoryChart" class="apex-charts" style="height:300px"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-container show">
|
<div class="panel-container show">
|
||||||
<div class="panel-content">
|
<div class="panel-content">
|
||||||
<div id="funnelChart" class="apex-charts"></div>
|
<div id="funnelChart" class="apex-charts" style="height:300px"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -337,10 +337,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
window.addEventListener('load', function() {
|
||||||
var days7 = <%- JSON.stringify(chartData.days7).replace(/</g, '\\u003c').replace(/>/g, '\\u003e') %>;
|
var days7 = <%- JSON.stringify(chartData.days).replace(/</g, '\\u003c').replace(/>/g, '\\u003e') %>;
|
||||||
var days30 = <%- JSON.stringify(chartData.days30).replace(/</g, '\\u003c').replace(/>/g, '\\u003e') %>;
|
var days30 = <%- JSON.stringify(chartData.days30).replace(/</g, '\\u003c').replace(/>/g, '\\u003e') %>;
|
||||||
var revenueData7 = <%- JSON.stringify(chartData.revenueData7).replace(/</g, '\\u003c').replace(/>/g, '\\u003e') %>;
|
var revenueData7 = <%- JSON.stringify(chartData.revenueData).replace(/</g, '\\u003c').replace(/>/g, '\\u003e') %>;
|
||||||
var revenueData30 = <%- JSON.stringify(chartData.revenueData30).replace(/</g, '\\u003c').replace(/>/g, '\\u003e') %>;
|
var revenueData30 = <%- JSON.stringify(chartData.revenueData30).replace(/</g, '\\u003c').replace(/>/g, '\\u003e') %>;
|
||||||
var catLabels = <%- JSON.stringify(chartData.revenueByCategory.map(function(c){ return c.name; })).replace(/</g, '\\u003c').replace(/>/g, '\\u003e') %>;
|
var catLabels = <%- JSON.stringify(chartData.revenueByCategory.map(function(c){ return c.name; })).replace(/</g, '\\u003c').replace(/>/g, '\\u003e') %>;
|
||||||
var catRevenue = <%- JSON.stringify(chartData.revenueByCategory.map(function(c){ return c.revenue; })).replace(/</g, '\\u003c').replace(/>/g, '\\u003e') %>;
|
var catRevenue = <%- JSON.stringify(chartData.revenueByCategory.map(function(c){ return c.revenue; })).replace(/</g, '\\u003c').replace(/>/g, '\\u003e') %>;
|
||||||
@@ -511,5 +511,5 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
})();
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -76,9 +76,12 @@
|
|||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
/* Draggable panel cursor */
|
/* Draggable panel cursor */
|
||||||
.panel-hdr { cursor: grab; }
|
.apex-charts {
|
||||||
.panel-hdr:active { cursor: grabbing; }
|
min-height: 200px;
|
||||||
.sortable-ghost { opacity: 0.4; }
|
}
|
||||||
|
.apex-charts.sparkline {
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -8,5 +8,8 @@
|
|||||||
<link rel="stylesheet" media="screen, print" href="/webfonts/smartadmin/sa-icons.css">
|
<link rel="stylesheet" media="screen, print" href="/webfonts/smartadmin/sa-icons.css">
|
||||||
<link rel="stylesheet" media="screen, print" href="/webfonts/fontawesome/fontawesome.css">
|
<link rel="stylesheet" media="screen, print" href="/webfonts/fontawesome/fontawesome.css">
|
||||||
|
|
||||||
|
<!-- ApexCharts (must be in head so it's available before page scripts) -->
|
||||||
|
<script src="/plugins/apexcharts/apexcharts.min.js"></script>
|
||||||
|
|
||||||
<!-- Save/Load functionality JavaScript -->
|
<!-- Save/Load functionality JavaScript -->
|
||||||
<script src="/scripts/core/saveloadscript.js"></script>
|
<script src="/scripts/core/saveloadscript.js"></script>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<!-- Core scripts -->
|
<!-- Core scripts -->
|
||||||
<script src="/plugins/bootstrap/bootstrap.bundle.min.js"></script>
|
<script src="/plugins/bootstrap/bootstrap.bundle.min.js"></script>
|
||||||
<script src="/plugins/apexcharts/apexcharts.min.js"></script>
|
|
||||||
<script src="/scripts/core/smartNavigation.js"></script>
|
<script src="/scripts/core/smartNavigation.js"></script>
|
||||||
<script src="/scripts/core/smartFilter.js"></script>
|
<script src="/scripts/core/smartFilter.js"></script>
|
||||||
<script src="/scripts/core/smartSlimscroll.js"></script>
|
<script src="/scripts/core/smartSlimscroll.js"></script>
|
||||||
@@ -12,26 +11,6 @@
|
|||||||
<!-- App.js -->
|
<!-- App.js -->
|
||||||
<script src="/scripts/core/smartApp.js"></script>
|
<script src="/scripts/core/smartApp.js"></script>
|
||||||
|
|
||||||
<!-- CSRF Token for all forms -->
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
var token = document.cookie.split('; ').find(function(c) { return c.indexOf('_csrf=') === 0; });
|
|
||||||
if (token) {
|
|
||||||
token = token.split('=')[1];
|
|
||||||
document.querySelectorAll('form[method="POST"], form[method="post"]').forEach(function(form) {
|
|
||||||
var input = document.createElement('input');
|
|
||||||
input.type = 'hidden';
|
|
||||||
input.name = '_csrf';
|
|
||||||
input.value = token;
|
|
||||||
form.appendChild(input);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Page specific scripts -->
|
|
||||||
<script src="/scripts/thirdparty/apexchartsWrapper.js" type="module"></script>
|
|
||||||
|
|
||||||
<!-- Theme persistence: save/restore body classes to localStorage -->
|
<!-- Theme persistence: save/restore body classes to localStorage -->
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
|
|||||||
Reference in New Issue
Block a user