feat: add pharmacy-product linking system
- Add pharmacy_products table (PostgreSQL + SQLite) - Add public route GET /api/products/:source/:id/pharmacies - Add admin CRUD routes for pharmacy-product links - Add cascade delete when pharmacy is removed - Update ProductView to show linked pharmacies - Create PharmacyProductLink admin component - Add 'Vincular Producto a Farmacia' tab in AdminView
This commit is contained in:
+193
-3
@@ -240,6 +240,38 @@ async function initDatabase() {
|
||||
await dbRun(`CREATE INDEX IF NOT EXISTS idx_pharmacy_medicine ON pharmacy_medicines(medicine_nregistro)`);
|
||||
}
|
||||
|
||||
// Create junction table for pharmacy-product relationships (CIMA / Open Food Facts)
|
||||
if (pgPool) {
|
||||
await pgPool.query(`
|
||||
CREATE TABLE IF NOT EXISTS pharmacy_products (
|
||||
id SERIAL PRIMARY KEY,
|
||||
pharmacy_id INTEGER NOT NULL REFERENCES pharmacies(id),
|
||||
product_source TEXT NOT NULL,
|
||||
product_off_id TEXT NOT NULL,
|
||||
product_name TEXT,
|
||||
price REAL,
|
||||
stock INTEGER DEFAULT 0,
|
||||
UNIQUE(pharmacy_id, product_source, product_off_id)
|
||||
)
|
||||
`);
|
||||
await pgPool.query(`CREATE INDEX IF NOT EXISTS idx_pharmacy_product ON pharmacy_products(product_source, product_off_id)`);
|
||||
} else {
|
||||
await dbRun(`
|
||||
CREATE TABLE IF NOT EXISTS pharmacy_products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
pharmacy_id INTEGER NOT NULL,
|
||||
product_source TEXT NOT NULL,
|
||||
product_off_id TEXT NOT NULL,
|
||||
product_name TEXT,
|
||||
price REAL,
|
||||
stock INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (pharmacy_id) REFERENCES pharmacies(id),
|
||||
UNIQUE(pharmacy_id, product_source, product_off_id)
|
||||
)
|
||||
`);
|
||||
await dbRun(`CREATE INDEX IF NOT EXISTS idx_pharmacy_product ON pharmacy_products(product_source, product_off_id)`);
|
||||
}
|
||||
|
||||
// Create users table — PG when available, SQLite fallback
|
||||
if (pgPool) {
|
||||
await pgPool.query(`
|
||||
@@ -647,6 +679,56 @@ app.get('/api/products/:source/:id', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get pharmacies that sell a specific product (by source and product ID)
|
||||
app.get('/api/products/:source/:productId/pharmacies', async (req, res) => {
|
||||
try {
|
||||
const { source, productId } = req.params;
|
||||
|
||||
if (source !== 'cima' && source !== 'openfoodfacts') {
|
||||
return res.status(400).json({ error: 'Invalid source' });
|
||||
}
|
||||
|
||||
let pharmacies = await userDbAll(`
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
p.address,
|
||||
p.phone,
|
||||
p.latitude,
|
||||
p.longitude,
|
||||
p.opening_hours,
|
||||
pp.price,
|
||||
pp.stock
|
||||
FROM pharmacies p
|
||||
INNER JOIN pharmacy_products pp ON p.id = pp.pharmacy_id
|
||||
WHERE pp.product_source = ? AND pp.product_off_id = ?
|
||||
ORDER BY p.name
|
||||
`, [source, productId]);
|
||||
|
||||
if (pharmacies.length === 0) {
|
||||
pharmacies = await userDbAll(`
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
p.address,
|
||||
p.phone,
|
||||
p.latitude,
|
||||
p.longitude,
|
||||
p.opening_hours,
|
||||
NULL as price,
|
||||
0 as stock
|
||||
FROM pharmacies p
|
||||
ORDER BY p.name
|
||||
`);
|
||||
}
|
||||
|
||||
res.json(pharmacies);
|
||||
} catch (error) {
|
||||
console.error('Error fetching pharmacies for product:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// ========== AUTHENTICATION MIDDLEWARE ==========
|
||||
|
||||
// Middleware to check if user is authenticated
|
||||
@@ -1680,10 +1762,9 @@ app.delete('/api/admin/pharmacies/:id', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const pharmacyId = parseInt(req.params.id);
|
||||
|
||||
// Delete related pharmacy_medicines first
|
||||
// Delete related pharmacy_medicines and pharmacy_products first
|
||||
await userDbRun('DELETE FROM pharmacy_medicines WHERE pharmacy_id = ?', [pharmacyId]);
|
||||
|
||||
// Delete the pharmacy
|
||||
await userDbRun('DELETE FROM pharmacy_products WHERE pharmacy_id = ?', [pharmacyId]);
|
||||
await userDbRun('DELETE FROM pharmacies WHERE id = ?', [pharmacyId]);
|
||||
|
||||
res.json({ message: 'Pharmacy deleted successfully' });
|
||||
@@ -1901,6 +1982,115 @@ app.delete('/api/admin/pharmacy-medicines/:id', requireAdmin, async (req, res) =
|
||||
}
|
||||
});
|
||||
|
||||
// ========== PHARMACY-PRODUCT ADMIN ROUTES ==========
|
||||
|
||||
// List products for a specific pharmacy
|
||||
app.get('/api/admin/pharmacies/:pharmacyId/products', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const pharmacyId = parseInt(req.params.pharmacyId);
|
||||
|
||||
const products = await userDbAll(`
|
||||
SELECT
|
||||
pp.id,
|
||||
pp.pharmacy_id,
|
||||
pp.product_source,
|
||||
pp.product_off_id,
|
||||
pp.product_name,
|
||||
pp.price,
|
||||
pp.stock
|
||||
FROM pharmacy_products pp
|
||||
WHERE pp.pharmacy_id = ?
|
||||
ORDER BY pp.product_name
|
||||
`, [pharmacyId]);
|
||||
|
||||
res.json(products);
|
||||
} catch (error) {
|
||||
console.error('Error fetching pharmacy products:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Add/upsert a product-pharmacy link
|
||||
app.post('/api/admin/pharmacy-products', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { pharmacy_id, product_source, product_off_id, product_name, price, stock } = req.body;
|
||||
|
||||
if (!pharmacy_id || !product_source || !product_off_id) {
|
||||
return res.status(400).json({ error: 'pharmacy_id, product_source, and product_off_id are required' });
|
||||
}
|
||||
|
||||
if (product_source !== 'cima' && product_source !== 'openfoodfacts') {
|
||||
return res.status(400).json({ error: 'product_source must be "cima" or "openfoodfacts"' });
|
||||
}
|
||||
|
||||
const existing = await userDbGet(
|
||||
'SELECT * FROM pharmacy_products WHERE pharmacy_id = ? AND product_source = ? AND product_off_id = ?',
|
||||
[pharmacy_id, product_source, product_off_id]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
await userDbRun(
|
||||
'UPDATE pharmacy_products SET product_name = ?, price = ?, stock = ? WHERE pharmacy_id = ? AND product_source = ? AND product_off_id = ?',
|
||||
[product_name || null, price || null, stock || 0, pharmacy_id, product_source, product_off_id]
|
||||
);
|
||||
} else {
|
||||
await userDbRun(
|
||||
'INSERT INTO pharmacy_products (pharmacy_id, product_source, product_off_id, product_name, price, stock) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[pharmacy_id, product_source, product_off_id, product_name || null, price || null, stock || 0]
|
||||
);
|
||||
}
|
||||
|
||||
const relationship = await userDbGet(
|
||||
'SELECT * FROM pharmacy_products WHERE pharmacy_id = ? AND product_source = ? AND product_off_id = ?',
|
||||
[pharmacy_id, product_source, product_off_id]
|
||||
);
|
||||
|
||||
res.status(201).json(relationship);
|
||||
} catch (error) {
|
||||
console.error('Error adding pharmacy product:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update price/stock for a pharmacy-product link
|
||||
app.put('/api/admin/pharmacy-products/:id', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const { price, stock } = req.body;
|
||||
|
||||
await userDbRun(
|
||||
'UPDATE pharmacy_products SET price = ?, stock = ? WHERE id = ?',
|
||||
[price || null, stock || 0, id]
|
||||
);
|
||||
|
||||
const updated = await userDbGet(
|
||||
'SELECT * FROM pharmacy_products WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!updated) {
|
||||
return res.status(404).json({ error: 'Relationship not found' });
|
||||
}
|
||||
|
||||
res.json(updated);
|
||||
} catch (error) {
|
||||
console.error('Error updating pharmacy product:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a pharmacy-product link
|
||||
app.delete('/api/admin/pharmacy-products/:id', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
await userDbRun('DELETE FROM pharmacy_products WHERE id = ?', [id]);
|
||||
res.json({ message: 'Product removed from pharmacy successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting pharmacy product:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Push notifications
|
||||
|
||||
app.get('/api/notifications/vapid-public-key', (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user