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 };