feat: Add server-side brand search to Intelligence page
- Backend: Add 'search' param to /api/admin/intelligence/brands - Frontend: Debounced search triggers server-side query - Now searches ALL brands, not just top 500 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,18 +21,30 @@ router.use(authMiddleware);
|
||||
*/
|
||||
router.get('/brands', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { limit = '500', offset = '0', state } = req.query;
|
||||
const { limit = '500', offset = '0', state, search } = req.query;
|
||||
const limitNum = Math.min(parseInt(limit as string, 10), 1000);
|
||||
const offsetNum = parseInt(offset as string, 10);
|
||||
|
||||
// Build WHERE clause based on state filter
|
||||
let stateFilter = '';
|
||||
// Build WHERE clause based on filters
|
||||
const conditions: string[] = ["sp.brand_name_raw IS NOT NULL AND sp.brand_name_raw != ''"];
|
||||
const params: any[] = [limitNum, offsetNum];
|
||||
let paramIndex = 3;
|
||||
|
||||
if (state && state !== 'all') {
|
||||
stateFilter = 'AND d.state = $3';
|
||||
conditions.push(`d.state = $${paramIndex}`);
|
||||
params.push(state);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Server-side search - case-insensitive partial match
|
||||
if (search && typeof search === 'string' && search.trim()) {
|
||||
conditions.push(`sp.brand_name_raw ILIKE $${paramIndex}`);
|
||||
params.push(`%${search.trim()}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(' AND ');
|
||||
|
||||
const { rows } = await pool.query(`
|
||||
SELECT
|
||||
sp.brand_name_raw as brand_name,
|
||||
@@ -43,26 +55,22 @@ router.get('/brands', async (req: Request, res: Response) => {
|
||||
ROUND(AVG(sp.price_med) FILTER (WHERE sp.price_med > 0)::numeric, 2) as avg_price_med
|
||||
FROM store_products sp
|
||||
JOIN dispensaries d ON sp.dispensary_id = d.id
|
||||
WHERE sp.brand_name_raw IS NOT NULL AND sp.brand_name_raw != ''
|
||||
${stateFilter}
|
||||
WHERE ${whereClause}
|
||||
GROUP BY sp.brand_name_raw
|
||||
ORDER BY store_count DESC, sku_count DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
`, params);
|
||||
|
||||
// Get total count with same state filter
|
||||
const countParams: any[] = [];
|
||||
let countStateFilter = '';
|
||||
if (state && state !== 'all') {
|
||||
countStateFilter = 'AND d.state = $1';
|
||||
countParams.push(state);
|
||||
}
|
||||
// Get total count with same filters (excluding limit/offset)
|
||||
const countParams = params.slice(2); // Remove limit and offset
|
||||
const countConditions = conditions.map((c, i) =>
|
||||
c.replace(/\$\d+/g, (match) => `$${parseInt(match.slice(1)) - 2}`)
|
||||
);
|
||||
const { rows: countRows } = await pool.query(`
|
||||
SELECT COUNT(DISTINCT sp.brand_name_raw) as total
|
||||
FROM store_products sp
|
||||
JOIN dispensaries d ON sp.dispensary_id = d.id
|
||||
WHERE sp.brand_name_raw IS NOT NULL AND sp.brand_name_raw != ''
|
||||
${countStateFilter}
|
||||
WHERE ${countConditions.join(' AND ')}
|
||||
`, countParams);
|
||||
|
||||
res.json({
|
||||
|
||||
Reference in New Issue
Block a user