219 lines
7.7 KiB
JavaScript
219 lines
7.7 KiB
JavaScript
const { all, audit, json, now, one, parseJson, run, tx } = require('../db');
|
|
|
|
function employeeFromRow(row) {
|
|
if (!row) return null;
|
|
return {
|
|
...row,
|
|
source_payload: parseJson(row.source_payload, null)
|
|
};
|
|
}
|
|
|
|
function assignmentFromRow(row) {
|
|
if (!row) return null;
|
|
return {
|
|
...row,
|
|
active: !row.released_at
|
|
};
|
|
}
|
|
|
|
function listEmployees(query = {}, companyId) {
|
|
return all(`
|
|
SELECT *
|
|
FROM employees
|
|
WHERE (? IS NULL OR entity_id = ?)
|
|
AND (? IS NULL OR status = ?)
|
|
AND (? IS NULL OR name LIKE '%' || ? || '%' OR email LIKE '%' || ? || '%' OR employee_number LIKE '%' || ? || '%')
|
|
ORDER BY name
|
|
LIMIT 500
|
|
`, [
|
|
companyId || null, companyId || null,
|
|
query.status || null,
|
|
query.status || null,
|
|
query.search || null,
|
|
query.search || null,
|
|
query.search || null,
|
|
query.search || null
|
|
]).map(employeeFromRow);
|
|
}
|
|
|
|
// Manual creates use the active company; Workday sync (no companyId) lands in the default company.
|
|
function upsertEmployee(employee, companyId) {
|
|
const timestamp = now();
|
|
const entityId = companyId || (one("SELECT id FROM entities WHERE status = 'active' ORDER BY id LIMIT 1")?.id) || null;
|
|
const workdayId = employee.workday_worker_id || employee.workdayWorkerId || employee.workerId || null;
|
|
const existing = workdayId
|
|
? one('SELECT id FROM employees WHERE workday_worker_id = ?', [workdayId])
|
|
: one('SELECT id FROM employees WHERE entity_id = ? AND (employee_number = ? OR lower(email) = lower(?))', [entityId, employee.employee_number || '', employee.email || '']);
|
|
|
|
const values = [
|
|
workdayId,
|
|
employee.employee_number || employee.employeeNumber || employee.workerNumber || null,
|
|
employee.name || employee.fullName || [employee.firstName, employee.lastName].filter(Boolean).join(' ') || 'Unnamed employee',
|
|
employee.email || employee.workEmail || null,
|
|
employee.department || employee.organization || null,
|
|
employee.location || employee.workLocation || null,
|
|
employee.manager_name || employee.managerName || null,
|
|
employee.status || 'active',
|
|
employee.source || 'manual',
|
|
json(employee.source_payload || employee),
|
|
employee.last_synced_at || null,
|
|
timestamp
|
|
];
|
|
|
|
if (existing) {
|
|
run(
|
|
`UPDATE employees SET
|
|
workday_worker_id = COALESCE(?, workday_worker_id),
|
|
employee_number = ?,
|
|
name = ?,
|
|
email = ?,
|
|
department = ?,
|
|
location = ?,
|
|
manager_name = ?,
|
|
status = ?,
|
|
source = ?,
|
|
source_payload = ?,
|
|
last_synced_at = COALESCE(?, last_synced_at),
|
|
updated_at = ?
|
|
WHERE id = ?`,
|
|
[...values, existing.id]
|
|
);
|
|
return one('SELECT * FROM employees WHERE id = ?', [existing.id]);
|
|
}
|
|
|
|
const result = run(
|
|
`INSERT INTO employees (
|
|
entity_id, workday_worker_id, employee_number, name, email, department, location, manager_name,
|
|
status, source, source_payload, last_synced_at, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[entityId, ...values, timestamp]
|
|
);
|
|
return one('SELECT * FROM employees WHERE id = ?', [result.lastInsertRowid]);
|
|
}
|
|
|
|
function assignmentRows(query = {}, companyId) {
|
|
return all(`
|
|
SELECT aa.*, a.asset_id AS asset_code, a.description AS asset_description,
|
|
e.name AS employee_name, e.email AS employee_email, e.employee_number
|
|
FROM asset_assignments aa
|
|
JOIN assets a ON a.id = aa.asset_id
|
|
LEFT JOIN employees e ON e.id = aa.employee_id
|
|
WHERE (? IS NULL OR a.entity_id = ?)
|
|
AND (? IS NULL OR aa.asset_id = ?)
|
|
AND (? IS NULL OR aa.employee_id = ?)
|
|
AND (? IS NULL OR aa.released_at IS NULL)
|
|
ORDER BY aa.assigned_at DESC, aa.id DESC
|
|
LIMIT 500
|
|
`, [
|
|
companyId || null, companyId || null,
|
|
query.asset_id || null,
|
|
query.asset_id || null,
|
|
query.employee_id || null,
|
|
query.employee_id || null,
|
|
query.active ? 1 : null
|
|
]).map(assignmentFromRow);
|
|
}
|
|
|
|
function assignAsset(body, userId, companyId) {
|
|
const timestamp = now();
|
|
// The asset must belong to the active company so assignments can't cross companies.
|
|
const asset = one('SELECT * FROM assets WHERE id = ? AND (? IS NULL OR entity_id = ?)', [body.asset_id, companyId || null, companyId || null]);
|
|
if (!asset) throw Object.assign(new Error('Asset not found'), { status: 404 });
|
|
|
|
// Only an employee in the same company may be assigned (foreign ids resolve to no employee).
|
|
const employee = body.employee_id ? one('SELECT * FROM employees WHERE id = ? AND (? IS NULL OR entity_id = ?)', [body.employee_id, companyId || null, companyId || null]) : null;
|
|
const department = body.department ?? employee?.department ?? asset.department ?? null;
|
|
const location = body.location ?? employee?.location ?? asset.location ?? null;
|
|
|
|
return tx(() => {
|
|
run(
|
|
`UPDATE asset_assignments
|
|
SET released_at = ?, status = 'released', release_reason = 'Reassigned', updated_at = ?
|
|
WHERE asset_id = ? AND released_at IS NULL`,
|
|
[timestamp, timestamp, body.asset_id]
|
|
);
|
|
|
|
const result = run(
|
|
`INSERT INTO asset_assignments (
|
|
asset_id, employee_id, department, location, status, assigned_at, assigned_by, notes, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
body.asset_id,
|
|
employee ? employee.id : null,
|
|
department,
|
|
location,
|
|
'assigned',
|
|
body.assigned_at || timestamp,
|
|
userId,
|
|
body.notes || null,
|
|
timestamp,
|
|
timestamp
|
|
]
|
|
);
|
|
|
|
run(
|
|
`UPDATE assets SET
|
|
custodian = ?,
|
|
department = ?,
|
|
location = ?,
|
|
status = 'in_service',
|
|
updated_at = ?
|
|
WHERE id = ?`,
|
|
[employee?.name || asset.custodian || null, department, location, timestamp, body.asset_id]
|
|
);
|
|
|
|
const assignment = assignmentRows({ asset_id: body.asset_id, active: true }, companyId)[0];
|
|
audit(userId, 'assign', 'asset', body.asset_id, null, assignment);
|
|
return assignment || one('SELECT * FROM asset_assignments WHERE id = ?', [result.lastInsertRowid]);
|
|
});
|
|
}
|
|
|
|
function releaseAssignment(id, body, userId, companyId) {
|
|
const before = one(
|
|
'SELECT aa.*, a.entity_id AS asset_entity_id FROM asset_assignments aa JOIN assets a ON a.id = aa.asset_id WHERE aa.id = ?',
|
|
[id]
|
|
);
|
|
if (!before || (companyId && before.asset_entity_id !== companyId)) return null;
|
|
const timestamp = now();
|
|
run(
|
|
`UPDATE asset_assignments
|
|
SET released_at = ?, status = 'released', release_reason = ?, notes = COALESCE(?, notes), updated_at = ?
|
|
WHERE id = ?`,
|
|
[timestamp, body.release_reason || 'Released', body.notes || null, timestamp, id]
|
|
);
|
|
audit(userId, 'release_assignment', 'asset_assignment', id, before, { released_at: timestamp, ...body });
|
|
return assignmentRows({ asset_id: before.asset_id }, companyId).find((assignment) => assignment.id === Number(id));
|
|
}
|
|
|
|
function assignmentSummary(companyId) {
|
|
const stats = one(`
|
|
SELECT
|
|
COUNT(*) AS active_assignments,
|
|
COUNT(DISTINCT aa.employee_id) AS assigned_employees,
|
|
COUNT(DISTINCT aa.location) AS assigned_locations
|
|
FROM asset_assignments aa
|
|
JOIN assets a ON a.id = aa.asset_id
|
|
WHERE aa.released_at IS NULL AND (? IS NULL OR a.entity_id = ?)
|
|
`, [companyId || null, companyId || null]);
|
|
const unassigned = one(`
|
|
SELECT COUNT(*) AS count
|
|
FROM assets a
|
|
WHERE (? IS NULL OR a.entity_id = ?)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM asset_assignments aa
|
|
WHERE aa.asset_id = a.id AND aa.released_at IS NULL
|
|
)
|
|
`, [companyId || null, companyId || null]);
|
|
return { ...stats, unassigned_assets: unassigned.count };
|
|
}
|
|
|
|
module.exports = {
|
|
assignAsset,
|
|
assignmentRows,
|
|
assignmentSummary,
|
|
employeeFromRow,
|
|
listEmployees,
|
|
releaseAssignment,
|
|
upsertEmployee
|
|
};
|