126 lines
3.9 KiB
TypeScript
Executable File
126 lines
3.9 KiB
TypeScript
Executable File
import { Router } from 'express';
|
|
import { authMiddleware } from '../auth/middleware';
|
|
import { pool } from '../db/migrate';
|
|
|
|
const router = Router();
|
|
router.use(authMiddleware);
|
|
|
|
// Get dashboard stats - consolidated DB (all tables in one DB now)
|
|
router.get('/stats', async (req, res) => {
|
|
try {
|
|
// Dispensary stats
|
|
const dispensariesResult = await pool.query(`
|
|
SELECT
|
|
COUNT(*) as total,
|
|
COUNT(*) FILTER (WHERE scrape_enabled = true) as active,
|
|
COUNT(*) FILTER (WHERE menu_url IS NOT NULL) as with_menu_url,
|
|
COUNT(*) FILTER (WHERE provider_type IS NOT NULL AND provider_type != 'unknown') as detected,
|
|
MIN(last_crawl_at) as oldest_crawl,
|
|
MAX(last_crawl_at) as latest_crawl
|
|
FROM dispensaries
|
|
`);
|
|
|
|
// Product stats from dutchie_products table
|
|
const productsResult = await pool.query(`
|
|
SELECT
|
|
COUNT(*) as total,
|
|
COUNT(*) FILTER (WHERE stock_status = 'in_stock') as in_stock,
|
|
COUNT(*) FILTER (WHERE primary_image_url IS NOT NULL) as with_images,
|
|
COUNT(DISTINCT brand_name) as unique_brands,
|
|
COUNT(DISTINCT dispensary_id) as stores_with_products
|
|
FROM dutchie_products
|
|
`);
|
|
|
|
// Campaign stats
|
|
const campaignsResult = await pool.query(`
|
|
SELECT
|
|
COUNT(*) as total,
|
|
COUNT(*) FILTER (WHERE active = true) as active
|
|
FROM campaigns
|
|
`);
|
|
|
|
// Recent clicks (last 24 hours)
|
|
const clicksResult = await pool.query(`
|
|
SELECT COUNT(*) as clicks_24h
|
|
FROM clicks
|
|
WHERE clicked_at >= NOW() - INTERVAL '24 hours'
|
|
`);
|
|
|
|
// Recent products added (last 24 hours)
|
|
const recentProductsResult = await pool.query(`
|
|
SELECT COUNT(*) as new_products_24h
|
|
FROM dutchie_products
|
|
WHERE created_at >= NOW() - INTERVAL '24 hours'
|
|
`);
|
|
|
|
// Proxy stats
|
|
const proxiesResult = await pool.query(`
|
|
SELECT
|
|
COUNT(*) as total,
|
|
COUNT(*) FILTER (WHERE active = true) as active,
|
|
COUNT(*) FILTER (WHERE is_anonymous = true) as anonymous
|
|
FROM proxies
|
|
`);
|
|
|
|
res.json({
|
|
stores: dispensariesResult.rows[0], // Keep 'stores' key for frontend compat
|
|
products: productsResult.rows[0],
|
|
campaigns: campaignsResult.rows[0],
|
|
clicks: clicksResult.rows[0],
|
|
recent: recentProductsResult.rows[0],
|
|
proxies: proxiesResult.rows[0]
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching dashboard stats:', error);
|
|
res.status(500).json({ error: 'Failed to fetch dashboard stats' });
|
|
}
|
|
});
|
|
|
|
// Get recent activity - from consolidated DB
|
|
router.get('/activity', async (req, res) => {
|
|
try {
|
|
const { limit = 20 } = req.query;
|
|
|
|
// Recent crawls from dispensaries
|
|
const scrapesResult = await pool.query(`
|
|
SELECT
|
|
COALESCE(d.dba_name, d.name) as name,
|
|
d.last_crawl_at as last_scraped_at,
|
|
(SELECT COUNT(*) FROM dutchie_products p WHERE p.dispensary_id = d.id) as product_count
|
|
FROM dispensaries d
|
|
WHERE d.last_crawl_at IS NOT NULL
|
|
ORDER BY d.last_crawl_at DESC
|
|
LIMIT $1
|
|
`, [limit]);
|
|
|
|
// Recent products from AZ pipeline
|
|
const productsResult = await pool.query(`
|
|
SELECT
|
|
p.name,
|
|
COALESCE(
|
|
(SELECT (options->0->>'price')::numeric
|
|
FROM dutchie_product_snapshots s
|
|
WHERE s.dutchie_product_id = p.id
|
|
ORDER BY crawled_at DESC LIMIT 1),
|
|
0
|
|
) as price,
|
|
COALESCE(d.dba_name, d.name) as store_name,
|
|
p.created_at as first_seen_at
|
|
FROM dutchie_products p
|
|
JOIN dispensaries d ON p.dispensary_id = d.id
|
|
ORDER BY p.created_at DESC
|
|
LIMIT $1
|
|
`, [limit]);
|
|
|
|
res.json({
|
|
recent_scrapes: scrapesResult.rows,
|
|
recent_products: productsResult.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching dashboard activity:', error);
|
|
res.status(500).json({ error: 'Failed to fetch dashboard activity' });
|
|
}
|
|
});
|
|
|
|
export default router;
|