## Responsive Admin UI - Layout.tsx: Mobile sidebar drawer with hamburger menu - Dashboard.tsx: 2-col grid on mobile, responsive stats cards - OrchestratorDashboard.tsx: Responsive table with hidden columns - PagesTab.tsx: Responsive filters and table ## SEO Pages - New /admin/seo section with state landing pages - SEO page generation and management - State page content with dispensary/product counts ## Click Analytics - Product click tracking infrastructure - Click analytics dashboard ## Other Changes - Consumer features scaffolding (alerts, deals, favorites) - Health panel component - Workers dashboard improvements - Legacy DutchieAZ pages removed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
134 lines
4.9 KiB
TypeScript
Executable File
134 lines
4.9 KiB
TypeScript
Executable File
import { Router } from 'express';
|
|
import { authMiddleware } from '../auth/middleware';
|
|
import { query as azQuery } from '../dutchie-az/db/connection';
|
|
|
|
const router = Router();
|
|
router.use(authMiddleware);
|
|
|
|
// Get dashboard stats - uses consolidated dutchie-az DB
|
|
// OPTIMIZED: Combined 4 sequential queries into 1 using CTEs
|
|
router.get('/stats', async (req, res) => {
|
|
try {
|
|
// All stats in a single query using CTEs
|
|
const result = await azQuery(`
|
|
WITH dispensary_stats AS (
|
|
SELECT
|
|
COUNT(*) as total,
|
|
COUNT(*) FILTER (WHERE menu_type IS NOT NULL AND menu_type != 'unknown') as active,
|
|
COUNT(*) FILTER (WHERE platform_dispensary_id IS NOT NULL) as with_platform_id,
|
|
COUNT(*) FILTER (WHERE menu_url IS NOT NULL) as with_menu_url,
|
|
MIN(last_crawled_at) as oldest_crawl,
|
|
MAX(last_crawled_at) as latest_crawl
|
|
FROM dispensaries
|
|
),
|
|
product_stats AS (
|
|
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) FILTER (WHERE brand_name IS NOT NULL AND brand_name != '') as unique_brands,
|
|
COUNT(DISTINCT dispensary_id) as dispensaries_with_products,
|
|
COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '24 hours') as new_products_24h
|
|
FROM dutchie_products
|
|
)
|
|
SELECT
|
|
ds.total as store_total, ds.active as store_active,
|
|
ds.with_platform_id as store_with_platform_id, ds.with_menu_url as store_with_menu_url,
|
|
ds.oldest_crawl, ds.latest_crawl,
|
|
ps.total as product_total, ps.in_stock as product_in_stock,
|
|
ps.with_images as product_with_images, ps.unique_brands as product_unique_brands,
|
|
ps.dispensaries_with_products, ps.new_products_24h
|
|
FROM dispensary_stats ds, product_stats ps
|
|
`);
|
|
|
|
const stats = result.rows[0] || {};
|
|
const storeStats = {
|
|
total: stats.store_total,
|
|
active: stats.store_active,
|
|
with_platform_id: stats.store_with_platform_id,
|
|
with_menu_url: stats.store_with_menu_url,
|
|
oldest_crawl: stats.oldest_crawl,
|
|
latest_crawl: stats.latest_crawl
|
|
};
|
|
const productStats = {
|
|
total: stats.product_total,
|
|
in_stock: stats.product_in_stock,
|
|
with_images: stats.product_with_images,
|
|
unique_brands: stats.product_unique_brands,
|
|
dispensaries_with_products: stats.dispensaries_with_products
|
|
};
|
|
|
|
res.json({
|
|
stores: {
|
|
total: parseInt(storeStats.total) || 0,
|
|
active: parseInt(storeStats.active) || 0,
|
|
with_menu_url: parseInt(storeStats.with_menu_url) || 0,
|
|
with_platform_id: parseInt(storeStats.with_platform_id) || 0,
|
|
oldest_crawl: storeStats.oldest_crawl,
|
|
latest_crawl: storeStats.latest_crawl
|
|
},
|
|
products: {
|
|
total: parseInt(productStats.total) || 0,
|
|
in_stock: parseInt(productStats.in_stock) || 0,
|
|
with_images: parseInt(productStats.with_images) || 0,
|
|
unique_brands: parseInt(productStats.unique_brands) || 0,
|
|
dispensaries_with_products: parseInt(productStats.dispensaries_with_products) || 0
|
|
},
|
|
brands: {
|
|
total: parseInt(productStats.unique_brands) || 0 // Same as unique_brands from product stats
|
|
},
|
|
campaigns: { total: 0, active: 0 }, // Legacy - no longer used
|
|
clicks: { clicks_24h: 0 }, // Legacy - no longer used
|
|
recent: { new_products_24h: parseInt(stats.new_products_24h) || 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 dutchie-az DB
|
|
router.get('/activity', async (req, res) => {
|
|
try {
|
|
const { limit = 20 } = req.query;
|
|
|
|
// Recent crawls from dispensaries (with product counts from dutchie_products)
|
|
const scrapesResult = await azQuery(`
|
|
SELECT
|
|
d.name,
|
|
d.last_crawled_at as last_scraped_at,
|
|
d.product_count
|
|
FROM dispensaries d
|
|
WHERE d.last_crawled_at IS NOT NULL
|
|
ORDER BY d.last_crawled_at DESC
|
|
LIMIT $1
|
|
`, [limit]);
|
|
|
|
// Recent products from dutchie_products
|
|
const productsResult = await azQuery(`
|
|
SELECT
|
|
p.name,
|
|
0 as price,
|
|
p.brand_name as brand,
|
|
p.thc as thc_percentage,
|
|
p.cbd as cbd_percentage,
|
|
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;
|