feat: implement web-based QR label print and audit endpoints with TDD tests
This commit is contained in:
212
server.js
212
server.js
@@ -712,6 +712,214 @@ app.post('/api/maps/save', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// 8. QR Asset Audit & Scan APIs
|
||||
// ==========================================
|
||||
|
||||
// GET all physical locations
|
||||
app.get('/api/physical-locations', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM physical_locations ORDER BY location_code');
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
handleError(res, err, 'GET PHYSICAL LOCATIONS');
|
||||
}
|
||||
});
|
||||
|
||||
// POST register scan (mobile)
|
||||
app.post('/api/audit/scan', async (req, res) => {
|
||||
let connection;
|
||||
try {
|
||||
const { asset_code, physical_location_code } = req.body;
|
||||
if (!asset_code || !physical_location_code) {
|
||||
return res.status(400).json({ error: 'asset_code and physical_location_code are required' });
|
||||
}
|
||||
|
||||
connection = await pool.getConnection();
|
||||
|
||||
// Verify if asset exists
|
||||
const [assets] = await connection.query('SELECT id FROM asset_core WHERE asset_code = ?', [asset_code]);
|
||||
if (assets.length === 0) {
|
||||
return res.status(404).json({ error: `Asset with code ${asset_code} not found` });
|
||||
}
|
||||
|
||||
// Insert pending audit record
|
||||
const [result] = await connection.query(
|
||||
'INSERT INTO asset_audit_pending (asset_code, physical_location_code, status) VALUES (?, ?, ?)',
|
||||
[asset_code, physical_location_code, 'PENDING']
|
||||
);
|
||||
|
||||
res.json({ success: true, pending_id: result.insertId });
|
||||
} catch (err) {
|
||||
handleError(res, err, 'REGISTER SCAN');
|
||||
} finally {
|
||||
if (connection) connection.release();
|
||||
}
|
||||
});
|
||||
|
||||
// GET pending audits list (admin)
|
||||
app.get('/api/audit/pending', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query(`
|
||||
SELECT
|
||||
ap.*,
|
||||
c.id AS asset_id,
|
||||
c.asset_purpose,
|
||||
c.asset_type,
|
||||
pl.location_name,
|
||||
pl.location_detail,
|
||||
pl.map_image,
|
||||
l.location AS old_location,
|
||||
l.location_detail AS old_location_detail
|
||||
FROM asset_audit_pending ap
|
||||
JOIN asset_core c ON c.asset_code = ap.asset_code
|
||||
JOIN physical_locations pl ON pl.location_code = ap.physical_location_code
|
||||
LEFT JOIN asset_location l ON l.asset_id = c.id AND l.is_active = 1
|
||||
ORDER BY ap.scanned_at DESC
|
||||
`);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
handleError(res, err, 'GET PENDING AUDITS');
|
||||
}
|
||||
});
|
||||
|
||||
// POST approve audits (admin)
|
||||
app.post('/api/audit/approve', async (req, res) => {
|
||||
let connection;
|
||||
try {
|
||||
const { pending_ids, processed_by } = req.body;
|
||||
if (!Array.isArray(pending_ids) || pending_ids.length === 0) {
|
||||
return res.status(400).json({ error: 'pending_ids must be a non-empty array' });
|
||||
}
|
||||
|
||||
connection = await pool.getConnection();
|
||||
await connection.beginTransaction();
|
||||
|
||||
let mapConfigChanged = false;
|
||||
let mapConfig = {};
|
||||
if (fs.existsSync('map_config.json')) {
|
||||
mapConfig = JSON.parse(fs.readFileSync('map_config.json', 'utf8') || '{}');
|
||||
}
|
||||
|
||||
for (const pendingId of pending_ids) {
|
||||
// 1. Get pending scan details
|
||||
const [pendings] = await connection.query(
|
||||
'SELECT asset_code, physical_location_code FROM asset_audit_pending WHERE id = ? AND status = ?',
|
||||
[pendingId, 'PENDING']
|
||||
);
|
||||
if (pendings.length === 0) continue;
|
||||
|
||||
const { asset_code, physical_location_code } = pendings[0];
|
||||
|
||||
// 2. Get asset ID
|
||||
const [assets] = await connection.query('SELECT id FROM asset_core WHERE asset_code = ?', [asset_code]);
|
||||
if (assets.length === 0) continue;
|
||||
const assetId = assets[0].id;
|
||||
|
||||
// 3. Get physical location details
|
||||
const [locations] = await connection.query(
|
||||
'SELECT location_name, location_detail, map_image, map_x, map_y FROM physical_locations WHERE location_code = ?',
|
||||
[physical_location_code]
|
||||
);
|
||||
if (locations.length === 0) continue;
|
||||
const loc = locations[0];
|
||||
|
||||
// 4. Deactivate old active locations for this asset
|
||||
await connection.query(
|
||||
'UPDATE asset_location SET is_active = 0, deactivated_at = NOW() WHERE asset_id = ? AND is_active = 1',
|
||||
[assetId]
|
||||
);
|
||||
|
||||
// 5. Insert new active location
|
||||
await connection.query(`
|
||||
INSERT INTO asset_location
|
||||
(asset_id, location, location_detail, location_photo, loc_x, loc_y, physical_location_code, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1)
|
||||
`, [assetId, loc.location_name, loc.location_detail, loc.map_image, loc.map_x, loc.map_y, physical_location_code]);
|
||||
|
||||
// 6. Update pending audit status
|
||||
await connection.query(
|
||||
'UPDATE asset_audit_pending SET status = ?, processed_at = NOW(), processed_by = ? WHERE id = ?',
|
||||
['APPROVED', processed_by || 'ADMIN', pendingId]
|
||||
);
|
||||
|
||||
// 7. Sync map_config.json
|
||||
// Remove asset from any other map coordinates
|
||||
for (const [mapPath, boxes] of Object.entries(mapConfig)) {
|
||||
let changed = false;
|
||||
const newBoxes = boxes.map(b => {
|
||||
if (b.asset_id === assetId) {
|
||||
changed = true;
|
||||
return { ...b, asset_id: null };
|
||||
}
|
||||
return b;
|
||||
});
|
||||
if (changed) {
|
||||
mapConfig[mapPath] = newBoxes;
|
||||
mapConfigChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add asset to the new map coordinate box matching map_image, map_x, map_y
|
||||
if (mapConfig[loc.map_image]) {
|
||||
const ax = parseFloat(loc.map_x);
|
||||
const ay = parseFloat(loc.map_y);
|
||||
const boxes = mapConfig[loc.map_image];
|
||||
const matchedBox = boxes.find(b => {
|
||||
const bx = parseFloat(b.x);
|
||||
const by = parseFloat(b.y);
|
||||
return Math.abs(bx - ax) < 0.1 && Math.abs(by - ay) < 0.1;
|
||||
});
|
||||
if (matchedBox) {
|
||||
matchedBox.asset_id = assetId;
|
||||
mapConfigChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mapConfigChanged) {
|
||||
fs.writeFileSync('map_config.json', JSON.stringify(mapConfig, null, 2));
|
||||
}
|
||||
|
||||
await connection.commit();
|
||||
res.json({ success: true, message: 'Audits approved successfully' });
|
||||
} catch (err) {
|
||||
if (connection) await connection.rollback();
|
||||
handleError(res, err, 'APPROVE AUDITS');
|
||||
} finally {
|
||||
if (connection) connection.release();
|
||||
}
|
||||
});
|
||||
|
||||
// POST reject audits (admin)
|
||||
app.post('/api/audit/reject', async (req, res) => {
|
||||
let connection;
|
||||
try {
|
||||
const { pending_ids, processed_by } = req.body;
|
||||
if (!Array.isArray(pending_ids) || pending_ids.length === 0) {
|
||||
return res.status(400).json({ error: 'pending_ids must be a non-empty array' });
|
||||
}
|
||||
|
||||
connection = await pool.getConnection();
|
||||
await connection.beginTransaction();
|
||||
|
||||
for (const pendingId of pending_ids) {
|
||||
await connection.query(
|
||||
'UPDATE asset_audit_pending SET status = ?, processed_at = NOW(), processed_by = ? WHERE id = ? AND status = ?',
|
||||
['REJECTED', processed_by || 'ADMIN', pendingId, 'PENDING']
|
||||
);
|
||||
}
|
||||
|
||||
await connection.commit();
|
||||
res.json({ success: true, message: 'Audits rejected successfully' });
|
||||
} catch (err) {
|
||||
if (connection) await connection.rollback();
|
||||
handleError(res, err, 'REJECT AUDITS');
|
||||
} finally {
|
||||
if (connection) connection.release();
|
||||
}
|
||||
});
|
||||
|
||||
// 7. File Upload API (Base64)
|
||||
app.post('/api/upload', (req, res) => {
|
||||
try {
|
||||
@@ -736,6 +944,6 @@ app.post('/api/upload', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(3000, '0.0.0.0', () => {
|
||||
console.log('📡 ITAM BACKEND SERVER RUNNING ON PORT 3000 (V3 Normalized)');
|
||||
app.listen(process.env.PORT || 3000, '0.0.0.0', () => {
|
||||
console.log(`📡 ITAM BACKEND SERVER RUNNING ON PORT ${process.env.PORT || 3000} (V3 Normalized)`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user