Add Dutchie AZ data pipeline and public API v1

- Add dutchie-az module with GraphQL product crawler, scheduler, and admin UI
- Add public API v1 endpoints (/api/v1/products, /categories, /brands, /specials, /menu)
- API key auth maps dispensary to dutchie_az store for per-dispensary data access
- Add frontend pages for Dutchie AZ stores, store details, and schedule management
- Update Layout with Dutchie AZ navigation section

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-02 09:43:26 -07:00
parent 511629b4e6
commit 917e91297e
22 changed files with 8201 additions and 45 deletions

View File

@@ -27,18 +27,18 @@ router.get('/', requireRole('superadmin', 'admin'), async (req, res) => {
}
});
// Get all stores for dropdown (must be before /:id to avoid route conflict)
router.get('/stores', requireRole('superadmin', 'admin'), async (req, res) => {
// Get all dispensaries for dropdown (must be before /:id to avoid route conflict)
router.get('/dispensaries', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const result = await pool.query(`
SELECT id, name
FROM stores
FROM dispensaries
ORDER BY name
`);
res.json({ stores: result.rows });
res.json({ dispensaries: result.rows });
} catch (error) {
console.error('Error fetching stores:', error);
res.status(500).json({ error: 'Failed to fetch stores' });
console.error('Error fetching dispensaries:', error);
res.status(500).json({ error: 'Failed to fetch dispensaries' });
}
});
@@ -67,22 +67,22 @@ router.get('/:id', requireRole('superadmin', 'admin'), async (req, res) => {
// Create new API permission
router.post('/', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const { user_name, allowed_ips, allowed_domains, store_id } = req.body;
const { user_name, allowed_ips, allowed_domains, dispensary_id } = req.body;
if (!user_name) {
return res.status(400).json({ error: 'User name is required' });
}
if (!store_id) {
return res.status(400).json({ error: 'Store is required' });
if (!dispensary_id) {
return res.status(400).json({ error: 'Dispensary is required' });
}
// Get store name for display
const storeResult = await pool.query('SELECT name FROM stores WHERE id = $1', [store_id]);
if (storeResult.rows.length === 0) {
return res.status(400).json({ error: 'Invalid store ID' });
// Get dispensary name for display
const dispensaryResult = await pool.query('SELECT name FROM dispensaries WHERE id = $1', [dispensary_id]);
if (dispensaryResult.rows.length === 0) {
return res.status(400).json({ error: 'Invalid dispensary ID' });
}
const storeName = storeResult.rows[0].name;
const dispensaryName = dispensaryResult.rows[0].name;
const apiKey = generateApiKey();
@@ -93,8 +93,8 @@ router.post('/', requireRole('superadmin', 'admin'), async (req, res) => {
allowed_ips,
allowed_domains,
is_active,
store_id,
store_name
dispensary_id,
dispensary_name
)
VALUES ($1, $2, $3, $4, 1, $5, $6)
RETURNING *
@@ -103,8 +103,8 @@ router.post('/', requireRole('superadmin', 'admin'), async (req, res) => {
apiKey,
allowed_ips || null,
allowed_domains || null,
store_id,
storeName
dispensary_id,
dispensaryName
]);
res.status(201).json({