Files
MixedAssets/server/services/contacts.js
2026-06-08 00:54:21 -05:00

181 lines
7.4 KiB
JavaScript

const { all, audit, json, now, one, parseJson, run } = require('../db');
const TYPES = ['employee', 'vendor', 'service_provider', 'contractor', 'work_location', 'other'];
const CREDIT_TERMS = ['na', 'net_30', 'net_60', 'net_90', 'net_180'];
const COLUMNS = [
'type', 'first_name', 'last_name', 'organization', 'email', 'phone',
'address_line1', 'address_line2', 'city', 'state_region', 'postal_code', 'country',
'notes', 'status', 'employee_id', 'tax_id', 'department', 'work_location',
'manager_first_name', 'manager_last_name', 'manager_email', 'start_date',
'rating', 'credit_term', 'source', 'workday_worker_id'
];
function contactNotes(contactId) {
return all(
`SELECT n.id, n.body, n.created_at, u.name AS author
FROM contact_notes n LEFT JOIN users u ON u.id = n.created_by
WHERE n.contact_id = ? ORDER BY n.id DESC`,
[contactId]
);
}
function displayName(row) {
const person = [row.first_name, row.last_name].filter(Boolean).join(' ').trim();
return row.organization || person || row.email || `Contact ${row.id}`;
}
function fromRow(row, withNotes = false) {
if (!row) return null;
const contact = { ...row, display_name: displayName(row) };
if (withNotes) contact.contact_notes = contactNotes(row.id);
return contact;
}
function normalize(body) {
const input = {};
for (const column of COLUMNS) {
if (Object.prototype.hasOwnProperty.call(body, column)) input[column] = body[column];
}
input.type = TYPES.includes(input.type) ? input.type : 'other';
input.status = input.status || 'active';
input.source = input.source || 'manual';
if (input.credit_term && !CREDIT_TERMS.includes(input.credit_term)) input.credit_term = 'na';
if (input.rating != null && input.rating !== '') input.rating = Math.max(0, Math.min(5, Number(input.rating)));
else input.rating = null;
if (input.workday_worker_id === '') input.workday_worker_id = null;
return input;
}
function listContacts(query = {}, companyId) {
return all(
`SELECT * FROM contacts
WHERE (? IS NULL OR entity_id = ?)
AND (? IS NULL OR type = ?)
AND (? IS NULL OR status = ?)
AND (? IS NULL OR (
first_name LIKE '%' || ? || '%' OR last_name LIKE '%' || ? || '%' OR
organization LIKE '%' || ? || '%' OR email LIKE '%' || ? || '%' OR
employee_id LIKE '%' || ? || '%'
))
ORDER BY type, organization, last_name, first_name`,
[
companyId || null, companyId || null,
query.type || null, query.type || null,
query.status || null, query.status || null,
query.search || null, query.search || null, query.search || null,
query.search || null, query.search || null, query.search || null
]
).map((row) => fromRow(row));
}
function getContact(id, companyId) {
return fromRow(one('SELECT * FROM contacts WHERE id = ? AND (? IS NULL OR entity_id = ?)', [id, companyId || null, companyId || null]), true);
}
function createContact(body, userId, companyId) {
const input = normalize(body);
const ts = now();
const cols = COLUMNS.filter((c) => input[c] !== undefined);
const result = run(
`INSERT INTO contacts (entity_id, ${cols.join(', ')}, created_by, created_at, updated_at)
VALUES (?, ${cols.map(() => '?').join(', ')}, ?, ?, ?)`,
[companyId || null, ...cols.map((c) => input[c]), userId, ts, ts]
);
audit(userId, 'create', 'contact', result.lastInsertRowid, null, { type: input.type, name: displayName(input) });
return getContact(result.lastInsertRowid, companyId);
}
function updateContact(id, body, userId, companyId) {
const before = one('SELECT * FROM contacts WHERE id = ?', [id]);
if (!before || (companyId && before.entity_id !== companyId)) return null;
const input = normalize({ ...before, ...body });
run(
`UPDATE contacts SET ${COLUMNS.map((c) => `${c} = ?`).join(', ')}, updated_at = ? WHERE id = ?`,
[...COLUMNS.map((c) => input[c] ?? null), now(), id]
);
audit(userId, 'update', 'contact', id, before, body);
return getContact(id, companyId);
}
function deleteContact(id, userId, companyId) {
const before = one('SELECT * FROM contacts WHERE id = ?', [id]);
if (!before || (companyId && before.entity_id !== companyId)) return false;
run('DELETE FROM contacts WHERE id = ?', [id]);
audit(userId, 'delete', 'contact', id, before, null);
return true;
}
function addNote(contactId, bodyText, userId, companyId) {
if (!one('SELECT id FROM contacts WHERE id = ? AND (? IS NULL OR entity_id = ?)', [contactId, companyId || null, companyId || null])) return null;
const result = run('INSERT INTO contact_notes (contact_id, body, created_by, created_at) VALUES (?, ?, ?, ?)', [contactId, bodyText, userId, now()]);
audit(userId, 'create', 'contact_note', result.lastInsertRowid, null, { contact_id: contactId });
return one('SELECT n.id, n.body, n.created_at, u.name AS author FROM contact_notes n LEFT JOIN users u ON u.id = n.created_by WHERE n.id = ?', [result.lastInsertRowid]);
}
function deleteNote(contactId, noteId, userId, companyId) {
if (companyId && !one('SELECT id FROM contacts WHERE id = ? AND entity_id = ?', [contactId, companyId])) return false;
const result = run('DELETE FROM contact_notes WHERE id = ? AND contact_id = ?', [noteId, contactId]);
if (!result.changes) return false;
audit(userId, 'delete', 'contact_note', noteId, null, { contact_id: contactId });
return true;
}
// Upsert an employee contact from a normalized Workday worker record.
function upsertWorkdayContact(worker) {
const ts = now();
const existing = worker.workday_worker_id
? one('SELECT id FROM contacts WHERE workday_worker_id = ?', [worker.workday_worker_id])
: (worker.email ? one('SELECT id FROM contacts WHERE type = ? AND lower(email) = lower(?)', ['employee', worker.email]) : null);
const fields = {
type: 'employee',
first_name: worker.first_name || null,
last_name: worker.last_name || null,
organization: null,
email: worker.email || null,
department: worker.department || null,
work_location: worker.location || null,
employee_id: worker.employee_number || null,
manager_first_name: worker.manager_first_name || null,
manager_last_name: worker.manager_last_name || null,
manager_email: worker.manager_email || null,
start_date: worker.start_date || null,
status: worker.status || 'active',
source: 'workday',
workday_worker_id: worker.workday_worker_id || null,
source_payload: json(worker.source_payload || worker)
};
if (existing) {
const cols = Object.keys(fields);
run(
`UPDATE contacts SET ${cols.map((c) => `${c} = COALESCE(?, ${c})`).join(', ')}, last_synced_at = ?, updated_at = ? WHERE id = ?`,
[...cols.map((c) => fields[c]), ts, ts, existing.id]
);
return existing.id;
}
// Workday sync is global (no request company context); new workers land in the default company.
const defaultCompany = one("SELECT id FROM entities WHERE status = 'active' ORDER BY id LIMIT 1");
const cols = Object.keys(fields);
const result = run(
`INSERT INTO contacts (entity_id, ${cols.join(', ')}, last_synced_at, created_at, updated_at) VALUES (?, ${cols.map(() => '?').join(', ')}, ?, ?, ?)`,
[defaultCompany ? defaultCompany.id : null, ...cols.map((c) => fields[c]), ts, ts, ts]
);
return result.lastInsertRowid;
}
module.exports = {
CREDIT_TERMS,
TYPES,
addNote,
createContact,
deleteContact,
deleteNote,
getContact,
listContacts,
parsePayload: (value) => parseJson(value, null),
updateContact,
upsertWorkdayContact
};