2026-03-19 05:42:11 +00:00
|
|
|
import { Router } from 'express';
|
|
|
|
|
import { pool } from '../db/pool';
|
|
|
|
|
import { requireAuth } from '../auth/middleware';
|
|
|
|
|
|
|
|
|
|
const router = Router();
|
|
|
|
|
router.use(requireAuth);
|
|
|
|
|
|
|
|
|
|
function camelToSnake(str: string): string {
|
|
|
|
|
return str.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GET /api/chemicals
|
|
|
|
|
router.get('/', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await pool.query(
|
2026-03-20 02:38:33 -05:00
|
|
|
'SELECT *, percentage_full::float AS percentage_full FROM chemicals WHERE user_id = $1 ORDER BY created_at DESC',
|
2026-03-19 05:42:11 +00:00
|
|
|
[req.user!.id]
|
|
|
|
|
);
|
|
|
|
|
// Map snake_case columns back to camelCase for the frontend
|
|
|
|
|
const rows = result.rows.map(snakeToCamel);
|
|
|
|
|
res.json(rows);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// POST /api/chemicals
|
|
|
|
|
router.post('/', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const b = req.body;
|
|
|
|
|
const result = await pool.query(`
|
|
|
|
|
INSERT INTO chemicals (
|
|
|
|
|
user_id, pi_first_name, physical_state, chemical_name, bldg_code, lab,
|
|
|
|
|
storage_location, storage_device, number_of_containers, amount_per_container,
|
|
|
|
|
unit_of_measure, cas_number,
|
|
|
|
|
chemical_formula, molecular_weight, vendor, catalog_number, lot_number,
|
|
|
|
|
expiration_date, concentration, percentage_full, needs_manual_entry,
|
|
|
|
|
scanned_image, comments, barcode, contact
|
|
|
|
|
) VALUES (
|
|
|
|
|
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,
|
|
|
|
|
$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25
|
|
|
|
|
) RETURNING *`,
|
|
|
|
|
[
|
|
|
|
|
req.user!.id, b.piFirstName, b.physicalState, b.chemicalName, b.bldgCode, b.lab,
|
|
|
|
|
b.storageLocation, b.storageDevice, b.numberOfContainers, b.amountPerContainer,
|
|
|
|
|
b.unitOfMeasure, b.casNumber,
|
2026-03-20 00:28:45 -05:00
|
|
|
b.chemicalFormula || null, b.molecularWeight || null, b.vendor || null,
|
|
|
|
|
b.catalogNumber || null, b.lotNumber || null,
|
|
|
|
|
b.expirationDate || null, b.concentration || null,
|
2026-03-19 05:42:11 +00:00
|
|
|
b.percentageFull ?? null, b.needsManualEntry ?? null,
|
2026-03-20 00:28:45 -05:00
|
|
|
b.scannedImage || null, b.comments || null, b.barcode || null, b.contact || null,
|
2026-03-19 05:42:11 +00:00
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
res.status(201).json(snakeToCamel(result.rows[0]));
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// PATCH /api/chemicals/:id
|
|
|
|
|
router.patch('/:id', async (req, res) => {
|
|
|
|
|
try {
|
2026-03-20 00:28:45 -05:00
|
|
|
const skip = new Set(['id', 'user_id', 'created_at', 'updated_at']);
|
|
|
|
|
const fields = Object.keys(req.body).filter(k => !skip.has(k) && !skip.has(camelToSnake(k)));
|
2026-03-19 05:42:11 +00:00
|
|
|
if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });
|
|
|
|
|
|
|
|
|
|
const snakeFields = fields.map(camelToSnake);
|
|
|
|
|
const setClauses = snakeFields.map((f, i) => `${f} = $${i + 3}`).join(', ');
|
2026-03-20 00:28:45 -05:00
|
|
|
const values = fields.map(f => req.body[f] || null);
|
2026-03-19 05:42:11 +00:00
|
|
|
|
|
|
|
|
const result = await pool.query(
|
|
|
|
|
`UPDATE chemicals SET ${setClauses}, updated_at = NOW()
|
|
|
|
|
WHERE id = $1 AND user_id = $2 RETURNING *`,
|
|
|
|
|
[req.params.id, req.user!.id, ...values]
|
|
|
|
|
);
|
|
|
|
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Not found' });
|
|
|
|
|
res.json(snakeToCamel(result.rows[0]));
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// DELETE /api/chemicals/:id
|
|
|
|
|
router.delete('/:id', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
await pool.query('DELETE FROM chemicals WHERE id = $1 AND user_id = $2',
|
|
|
|
|
[req.params.id, req.user!.id]);
|
|
|
|
|
res.status(204).end();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function snakeToCamel(row: Record<string, unknown>): Record<string, unknown> {
|
|
|
|
|
return Object.fromEntries(
|
|
|
|
|
Object.entries(row).map(([k, v]) => [
|
|
|
|
|
k.replace(/_([a-z])/g, (_, l) => l.toUpperCase()),
|
|
|
|
|
v,
|
|
|
|
|
])
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default router;
|