feat: add pharmacy-product linking system
Run Tests on Branches / Detect Changes (push) Successful in 12s
Run Tests on Branches / Frontend Tests (push) Has been cancelled
Run Tests on Branches / Frontend Mobile Tests (push) Has been cancelled
Run Tests on Branches / Backend Tests (push) Has been cancelled

- 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:
Antoni Nuñez Romeu
2026-07-13 20:41:16 +02:00
parent f921b15d67
commit 8bf0b33249
8 changed files with 794 additions and 4 deletions
+193 -3
View File
@@ -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) => {