Merge pull request 'feat: custom violation types — persist, manage, and use in violation form' (#46) from claude/musing-bell into master
Reviewed-on: #46
This commit was merged in pull request #46.
This commit is contained in:
94
server.js
94
server.js
@@ -463,6 +463,100 @@ app.get('/api/audit', (req, res) => {
|
||||
res.json(db.prepare(sql).all(...args));
|
||||
});
|
||||
|
||||
// ── Custom Violation Types ────────────────────────────────────────────────────
|
||||
// Persisted violation type definitions stored in violation_types table.
|
||||
// type_key is auto-generated (custom_<slug>) to avoid collisions with hardcoded keys.
|
||||
|
||||
app.get('/api/violation-types', (req, res) => {
|
||||
const rows = db.prepare('SELECT * FROM violation_types ORDER BY category ASC, name ASC').all();
|
||||
res.json(rows.map(r => ({ ...r, fields: JSON.parse(r.fields) })));
|
||||
});
|
||||
|
||||
app.post('/api/violation-types', (req, res) => {
|
||||
const { name, category, chapter, description, min_points, max_points, fields, created_by } = req.body;
|
||||
if (!name || !name.trim()) return res.status(400).json({ error: 'name is required' });
|
||||
|
||||
const minPts = parseInt(min_points) || 1;
|
||||
const maxPts = parseInt(max_points) || minPts;
|
||||
if (maxPts < minPts) return res.status(400).json({ error: 'max_points must be >= min_points' });
|
||||
|
||||
// Generate a unique type_key from the name, prefixed with 'custom_'
|
||||
const base = 'custom_' + name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
||||
let typeKey = base;
|
||||
let suffix = 2;
|
||||
while (db.prepare('SELECT id FROM violation_types WHERE type_key = ?').get(typeKey)) {
|
||||
typeKey = `${base}_${suffix++}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = db.prepare(`
|
||||
INSERT INTO violation_types (type_key, name, category, chapter, description, min_points, max_points, fields)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
typeKey,
|
||||
name.trim(),
|
||||
(category || 'Custom').trim(),
|
||||
chapter || null,
|
||||
description || null,
|
||||
minPts,
|
||||
maxPts,
|
||||
JSON.stringify(fields && fields.length ? fields : ['description'])
|
||||
);
|
||||
const row = db.prepare('SELECT * FROM violation_types WHERE id = ?').get(result.lastInsertRowid);
|
||||
audit('violation_type_created', 'violation_type', result.lastInsertRowid, created_by || null, { name: row.name, category: row.category });
|
||||
res.status(201).json({ ...row, fields: JSON.parse(row.fields) });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/violation-types/:id', (req, res) => {
|
||||
const id = parseInt(req.params.id);
|
||||
const row = db.prepare('SELECT * FROM violation_types WHERE id = ?').get(id);
|
||||
if (!row) return res.status(404).json({ error: 'Violation type not found' });
|
||||
|
||||
const { name, category, chapter, description, min_points, max_points, fields, updated_by } = req.body;
|
||||
if (!name || !name.trim()) return res.status(400).json({ error: 'name is required' });
|
||||
|
||||
const minPts = parseInt(min_points) || 1;
|
||||
const maxPts = parseInt(max_points) || minPts;
|
||||
if (maxPts < minPts) return res.status(400).json({ error: 'max_points must be >= min_points' });
|
||||
|
||||
db.prepare(`
|
||||
UPDATE violation_types
|
||||
SET name=?, category=?, chapter=?, description=?, min_points=?, max_points=?, fields=?, updated_at=CURRENT_TIMESTAMP
|
||||
WHERE id=?
|
||||
`).run(
|
||||
name.trim(),
|
||||
(category || 'Custom').trim(),
|
||||
chapter || null,
|
||||
description || null,
|
||||
minPts,
|
||||
maxPts,
|
||||
JSON.stringify(fields && fields.length ? fields : ['description']),
|
||||
id
|
||||
);
|
||||
|
||||
const updated = db.prepare('SELECT * FROM violation_types WHERE id = ?').get(id);
|
||||
audit('violation_type_updated', 'violation_type', id, updated_by || null, { name: updated.name, category: updated.category });
|
||||
res.json({ ...updated, fields: JSON.parse(updated.fields) });
|
||||
});
|
||||
|
||||
app.delete('/api/violation-types/:id', (req, res) => {
|
||||
const id = parseInt(req.params.id);
|
||||
const row = db.prepare('SELECT * FROM violation_types WHERE id = ?').get(id);
|
||||
if (!row) return res.status(404).json({ error: 'Violation type not found' });
|
||||
|
||||
const usage = db.prepare('SELECT COUNT(*) as count FROM violations WHERE violation_type = ?').get(row.type_key);
|
||||
if (usage.count > 0) {
|
||||
return res.status(409).json({ error: `Cannot delete: ${usage.count} violation(s) reference this type. Negate those violations first.` });
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM violation_types WHERE id = ?').run(id);
|
||||
audit('violation_type_deleted', 'violation_type', id, null, { name: row.name, type_key: row.type_key });
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// ── PDF endpoint ─────────────────────────────────────────────────────────────
|
||||
app.get('/api/violations/:id/pdf', async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user