Updated Asset Entry and Views
This commit is contained in:
@@ -327,6 +327,14 @@ function initialize() {
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS asset_notes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
||||
body TEXT NOT NULL,
|
||||
created_by INTEGER REFERENCES users(id),
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
actor_id INTEGER REFERENCES users(id),
|
||||
|
||||
@@ -70,6 +70,50 @@ router.post('/assets/:id/files', upload.single('file'), requireRole('admin', 'fi
|
||||
res.status(201).json({ file: one('SELECT id, file_name, mime_type, size, uploaded_at FROM asset_files WHERE id = ?', [result.lastInsertRowid]) });
|
||||
});
|
||||
|
||||
router.get('/assets/:id/files/:fileId', (req, res) => {
|
||||
const file = one('SELECT * FROM asset_files WHERE id = ? AND asset_id = ?', [req.params.fileId, req.params.id]);
|
||||
if (!file) return res.status(404).json({ error: 'File not found' });
|
||||
res.setHeader('Content-Type', file.mime_type || 'application/octet-stream');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${file.file_name}"`);
|
||||
return res.send(Buffer.from(file.content_base64, 'base64'));
|
||||
});
|
||||
|
||||
router.delete('/assets/:id/files/:fileId', requireRole('admin', 'finance', 'operations'), (req, res) => {
|
||||
const result = run('DELETE FROM asset_files WHERE id = ? AND asset_id = ?', [req.params.fileId, req.params.id]);
|
||||
if (!result.changes) return res.status(404).json({ error: 'File not found' });
|
||||
audit(req.user.id, 'delete', 'asset_file', req.params.fileId, null, { asset_id: req.params.id });
|
||||
return res.status(204).end();
|
||||
});
|
||||
|
||||
router.post('/assets/:id/warranties', requireRole('admin', 'finance', 'operations'), (req, res) => {
|
||||
const b = req.body;
|
||||
const result = run(
|
||||
`INSERT INTO warranties (
|
||||
asset_id, provider, phone, start_date, expiration_date, warranty_limit,
|
||||
actual_usage, coverage_details, document_name, document_base64, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
req.params.id, b.provider || null, b.phone || null, b.start_date || null, b.expiration_date || null,
|
||||
b.warranty_limit || null, b.actual_usage || null, b.coverage_details || null,
|
||||
b.document_name || null, b.document_base64 || null, now()
|
||||
]
|
||||
);
|
||||
audit(req.user.id, 'create', 'warranty', result.lastInsertRowid, null, { ...b, document_base64: b.document_base64 ? '[stored]' : null });
|
||||
res.status(201).json({
|
||||
warranty: one(
|
||||
'SELECT id, provider, phone, start_date, expiration_date, warranty_limit, actual_usage, coverage_details, document_name, created_at FROM warranties WHERE id = ?',
|
||||
[result.lastInsertRowid]
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
router.delete('/assets/:id/warranties/:warrantyId', requireRole('admin', 'finance', 'operations'), (req, res) => {
|
||||
const result = run('DELETE FROM warranties WHERE id = ? AND asset_id = ?', [req.params.warrantyId, req.params.id]);
|
||||
if (!result.changes) return res.status(404).json({ error: 'Warranty not found' });
|
||||
audit(req.user.id, 'delete', 'warranty', req.params.warrantyId, null, { asset_id: req.params.id });
|
||||
return res.status(204).end();
|
||||
});
|
||||
|
||||
router.post('/assets/:id/tasks', requireRole('admin', 'finance', 'operations'), (req, res) => {
|
||||
const created = now();
|
||||
const result = run(
|
||||
@@ -80,4 +124,52 @@ router.post('/assets/:id/tasks', requireRole('admin', 'finance', 'operations'),
|
||||
res.status(201).json({ task: one('SELECT * FROM tasks WHERE id = ?', [result.lastInsertRowid]) });
|
||||
});
|
||||
|
||||
router.put('/assets/:id/tasks/:taskId', requireRole('admin', 'finance', 'operations'), (req, res) => {
|
||||
const before = one('SELECT * FROM tasks WHERE id = ? AND asset_id = ?', [req.params.taskId, req.params.id]);
|
||||
if (!before) return res.status(404).json({ error: 'Task not found' });
|
||||
run(
|
||||
'UPDATE tasks SET title = ?, type = ?, status = ?, due_date = ?, notes = ?, updated_at = ? WHERE id = ?',
|
||||
[
|
||||
req.body.title ?? before.title,
|
||||
req.body.type ?? before.type,
|
||||
req.body.status ?? before.status,
|
||||
req.body.due_date ?? before.due_date,
|
||||
req.body.notes ?? before.notes,
|
||||
now(),
|
||||
req.params.taskId
|
||||
]
|
||||
);
|
||||
audit(req.user.id, 'update', 'task', req.params.taskId, before, req.body);
|
||||
return res.json({ task: one('SELECT * FROM tasks WHERE id = ?', [req.params.taskId]) });
|
||||
});
|
||||
|
||||
router.delete('/assets/:id/tasks/:taskId', requireRole('admin', 'finance', 'operations'), (req, res) => {
|
||||
const result = run('DELETE FROM tasks WHERE id = ? AND asset_id = ?', [req.params.taskId, req.params.id]);
|
||||
if (!result.changes) return res.status(404).json({ error: 'Task not found' });
|
||||
audit(req.user.id, 'delete', 'task', req.params.taskId, null, { asset_id: req.params.id });
|
||||
return res.status(204).end();
|
||||
});
|
||||
|
||||
router.post('/assets/:id/notes', requireRole('admin', 'finance', 'operations'), (req, res) => {
|
||||
if (!req.body.body) return res.status(400).json({ error: 'Note body is required' });
|
||||
const result = run(
|
||||
'INSERT INTO asset_notes (asset_id, body, created_by, created_at) VALUES (?, ?, ?, ?)',
|
||||
[req.params.id, req.body.body, req.user.id, now()]
|
||||
);
|
||||
audit(req.user.id, 'create', 'asset_note', result.lastInsertRowid, null, { asset_id: req.params.id });
|
||||
res.status(201).json({
|
||||
note: one(
|
||||
'SELECT n.id, n.body, n.created_at, u.name AS author FROM asset_notes n LEFT JOIN users u ON u.id = n.created_by WHERE n.id = ?',
|
||||
[result.lastInsertRowid]
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
router.delete('/assets/:id/notes/:noteId', requireRole('admin', 'finance', 'operations'), (req, res) => {
|
||||
const result = run('DELETE FROM asset_notes WHERE id = ? AND asset_id = ?', [req.params.noteId, req.params.id]);
|
||||
if (!result.changes) return res.status(404).json({ error: 'Note not found' });
|
||||
audit(req.user.id, 'delete', 'asset_note', req.params.noteId, null, { asset_id: req.params.id });
|
||||
return res.status(204).end();
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -63,6 +63,14 @@ function hydrateAsset(id) {
|
||||
asset.tasks = all('SELECT * FROM tasks WHERE asset_id = ? ORDER BY due_date IS NULL, due_date', [id]);
|
||||
asset.warranties = all('SELECT id, provider, phone, start_date, expiration_date, warranty_limit, actual_usage, coverage_details, document_name, created_at FROM warranties WHERE asset_id = ?', [id]);
|
||||
asset.files = all('SELECT id, file_name, mime_type, size, uploaded_at FROM asset_files WHERE asset_id = ?', [id]);
|
||||
asset.notes_log = all(
|
||||
`SELECT n.id, n.body, n.created_at, u.name AS author
|
||||
FROM asset_notes n
|
||||
LEFT JOIN users u ON u.id = n.created_by
|
||||
WHERE n.asset_id = ?
|
||||
ORDER BY n.id DESC`,
|
||||
[id]
|
||||
);
|
||||
return asset;
|
||||
}
|
||||
|
||||
|
||||
@@ -141,10 +141,71 @@ async function main() {
|
||||
acquired_date: '2026-01-05',
|
||||
in_service_date: '2026-01-05',
|
||||
useful_life_months: 60,
|
||||
depreciation_method: 'macrs_gds_200db_5'
|
||||
depreciation_method: 'macrs_gds_200db_5',
|
||||
books: [
|
||||
{ book_type: 'GAAP', active: true, depreciation_method: 'straight_line', convention: 'half_year', useful_life_months: 60, cost: 2400 },
|
||||
{ book_type: 'FEDERAL', active: true, depreciation_method: 'macrs_gds_200db_5', convention: 'half_year', useful_life_months: 60, section_179_amount: 500, bonus_percent: 0, cost: 2400 }
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const assetId = assetResult.asset.id;
|
||||
|
||||
// Per-book persistence: GAAP and FEDERAL must keep distinct methods.
|
||||
const hydrated = await request(`/api/assets/${assetId}`, { headers: auth });
|
||||
const gaapBook = hydrated.asset.books.find((book) => book.book_type === 'GAAP');
|
||||
const federalBook = hydrated.asset.books.find((book) => book.book_type === 'FEDERAL');
|
||||
if (gaapBook?.depreciation_method !== 'straight_line' || federalBook?.depreciation_method !== 'macrs_gds_200db_5') {
|
||||
throw new Error('Per-book depreciation methods did not persist independently');
|
||||
}
|
||||
if (Number(federalBook.section_179_amount) !== 500) {
|
||||
throw new Error('Per-book Section 179 amount did not persist');
|
||||
}
|
||||
|
||||
// File cabinet (multipart upload bypasses the JSON request helper)
|
||||
const uploadForm = new FormData();
|
||||
uploadForm.append('file', new Blob(['invoice contents'], { type: 'text/plain' }), 'invoice.txt');
|
||||
const uploadResponse = await fetch(`${baseUrl}/api/assets/${assetId}/files`, { method: 'POST', headers: auth, body: uploadForm });
|
||||
const fileResult = await uploadResponse.json();
|
||||
if (!uploadResponse.ok || !fileResult.file?.id) throw new Error('Asset file upload did not return a file');
|
||||
const fileDownload = await fetch(`${baseUrl}/api/assets/${assetId}/files/${fileResult.file.id}`, { headers: auth });
|
||||
if (!fileDownload.ok || (await fileDownload.text()) !== 'invoice contents') {
|
||||
throw new Error('Asset file download did not return stored contents');
|
||||
}
|
||||
|
||||
// Warranty
|
||||
const warrantyResult = await request(`/api/assets/${assetId}/warranties`, {
|
||||
method: 'POST',
|
||||
headers: auth,
|
||||
body: JSON.stringify({ provider: 'Acme Care', start_date: '2026-01-05', expiration_date: '2029-01-05', coverage_details: 'Parts and labor' })
|
||||
});
|
||||
if (!warrantyResult.warranty?.id) throw new Error('Warranty was not created');
|
||||
|
||||
// Task lifecycle
|
||||
const taskResult = await request(`/api/assets/${assetId}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: auth,
|
||||
body: JSON.stringify({ title: 'Calibrate device', type: 'calibration', due_date: '2026-03-01' })
|
||||
});
|
||||
const completedTask = await request(`/api/assets/${assetId}/tasks/${taskResult.task.id}`, {
|
||||
method: 'PUT',
|
||||
headers: auth,
|
||||
body: JSON.stringify({ status: 'complete' })
|
||||
});
|
||||
if (completedTask.task.status !== 'complete') throw new Error('Task status update did not persist');
|
||||
|
||||
// Notes
|
||||
await request(`/api/assets/${assetId}/notes`, {
|
||||
method: 'POST',
|
||||
headers: auth,
|
||||
body: JSON.stringify({ body: 'Received and tagged in receiving dock.' })
|
||||
});
|
||||
|
||||
const detailed = await request(`/api/assets/${assetId}`, { headers: auth });
|
||||
if (!detailed.asset.files.length || !detailed.asset.warranties.length || !detailed.asset.notes_log.length) {
|
||||
throw new Error('Asset detail collections (files/warranties/notes) were not returned');
|
||||
}
|
||||
|
||||
const employeeResult = await request('/api/employees', {
|
||||
method: 'POST',
|
||||
headers: auth,
|
||||
|
||||
Reference in New Issue
Block a user