Major additions: - Multi-state expansion: states table, StateSelector, NationalDashboard, StateHeatmap, CrossStateCompare - Orchestrator services: trace service, error taxonomy, retry manager, proxy rotator - Discovery system: dutchie discovery service, geo validation, city seeding scripts - Analytics infrastructure: analytics v2 routes, brand/pricing/stores intelligence pages - Local development: setup-local.sh starts all 5 services (postgres, backend, cannaiq, findadispo, findagram) - Migrations 037-056: crawler profiles, states, analytics indexes, worker metadata Frontend pages added: - Discovery, ChainsDashboard, IntelligenceBrands, IntelligencePricing, IntelligenceStores - StateHeatmap, CrossStateCompare, SyncInfoPanel Components added: - StateSelector, OrchestratorTraceModal, WorkflowStepper 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
173 lines
4.5 KiB
TypeScript
173 lines
4.5 KiB
TypeScript
import { Router } from 'express';
|
|
import { authMiddleware } from '../auth/middleware';
|
|
import { pool } from '../db/pool';
|
|
|
|
const router = Router();
|
|
router.use(authMiddleware);
|
|
|
|
// Get all changes with optional status filter
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const { status } = req.query;
|
|
|
|
let query = `
|
|
SELECT
|
|
dc.id,
|
|
dc.dispensary_id,
|
|
dc.field_name,
|
|
dc.old_value,
|
|
dc.new_value,
|
|
dc.source,
|
|
dc.confidence_score,
|
|
dc.change_notes,
|
|
dc.status,
|
|
dc.requires_recrawl,
|
|
dc.created_at,
|
|
dc.reviewed_at,
|
|
dc.reviewed_by,
|
|
dc.rejection_reason,
|
|
d.name as dispensary_name,
|
|
d.slug as dispensary_slug,
|
|
d.city,
|
|
d.state
|
|
FROM dispensary_changes dc
|
|
JOIN dispensaries d ON dc.dispensary_id = d.id
|
|
`;
|
|
|
|
const params: any[] = [];
|
|
|
|
if (status) {
|
|
query += ` WHERE dc.status = $1`;
|
|
params.push(status);
|
|
}
|
|
|
|
query += ` ORDER BY dc.created_at DESC`;
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
res.json({ changes: result.rows });
|
|
} catch (error) {
|
|
console.error('Error fetching changes:', error);
|
|
res.status(500).json({ error: 'Failed to fetch changes' });
|
|
}
|
|
});
|
|
|
|
// Get changes statistics (for alert banner)
|
|
router.get('/stats', async (req, res) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE status = 'pending') as pending_count,
|
|
COUNT(*) FILTER (WHERE status = 'pending' AND requires_recrawl = TRUE) as pending_recrawl_count,
|
|
COUNT(*) FILTER (WHERE status = 'approved') as approved_count,
|
|
COUNT(*) FILTER (WHERE status = 'rejected') as rejected_count
|
|
FROM dispensary_changes
|
|
`);
|
|
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
console.error('Error fetching change stats:', error);
|
|
res.status(500).json({ error: 'Failed to fetch change stats' });
|
|
}
|
|
});
|
|
|
|
// Approve a change and apply it to the dispensary
|
|
router.post('/:id/approve', async (req, res) => {
|
|
const client = await pool.connect();
|
|
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
const { id } = req.params;
|
|
const userId = (req as any).user?.id; // From auth middleware
|
|
|
|
// Get the change record
|
|
const changeResult = await client.query(`
|
|
SELECT * FROM dispensary_changes WHERE id = $1 AND status = 'pending'
|
|
`, [id]);
|
|
|
|
if (changeResult.rows.length === 0) {
|
|
await client.query('ROLLBACK');
|
|
return res.status(404).json({ error: 'Pending change not found' });
|
|
}
|
|
|
|
const change = changeResult.rows[0];
|
|
|
|
// Apply the change to the dispensary table
|
|
const updateQuery = `
|
|
UPDATE dispensaries
|
|
SET ${change.field_name} = $1, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $2
|
|
RETURNING *
|
|
`;
|
|
|
|
const dispensaryResult = await client.query(updateQuery, [
|
|
change.new_value,
|
|
change.dispensary_id
|
|
]);
|
|
|
|
if (dispensaryResult.rows.length === 0) {
|
|
await client.query('ROLLBACK');
|
|
return res.status(404).json({ error: 'Dispensary not found' });
|
|
}
|
|
|
|
// Mark the change as approved
|
|
await client.query(`
|
|
UPDATE dispensary_changes
|
|
SET
|
|
status = 'approved',
|
|
reviewed_at = CURRENT_TIMESTAMP,
|
|
reviewed_by = $1
|
|
WHERE id = $2
|
|
`, [userId, id]);
|
|
|
|
await client.query('COMMIT');
|
|
|
|
res.json({
|
|
message: 'Change approved and applied',
|
|
dispensary: dispensaryResult.rows[0],
|
|
requires_recrawl: change.requires_recrawl
|
|
});
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
console.error('Error approving change:', error);
|
|
res.status(500).json({ error: 'Failed to approve change' });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
});
|
|
|
|
// Reject a change with optional reason
|
|
router.post('/:id/reject', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { reason } = req.body;
|
|
const userId = (req as any).user?.id; // From auth middleware
|
|
|
|
const result = await pool.query(`
|
|
UPDATE dispensary_changes
|
|
SET
|
|
status = 'rejected',
|
|
reviewed_at = CURRENT_TIMESTAMP,
|
|
reviewed_by = $1,
|
|
rejection_reason = $2
|
|
WHERE id = $3 AND status = 'pending'
|
|
RETURNING *
|
|
`, [userId, reason, id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Pending change not found' });
|
|
}
|
|
|
|
res.json({
|
|
message: 'Change rejected',
|
|
change: result.rows[0]
|
|
});
|
|
} catch (error) {
|
|
console.error('Error rejecting change:', error);
|
|
res.status(500).json({ error: 'Failed to reject change' });
|
|
}
|
|
});
|
|
|
|
export default router;
|