From ef92c86fcc909bbc55c78596a3800305e0fbf360 Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Sun, 18 Jan 2026 13:27:02 -0800 Subject: [PATCH] Add team member search/add with phone or email support --- api/Application.cfm | 2 + api/portal/addTeamMember.cfm | 72 +++++++++++++++++++++++++++++ api/portal/searchUser.cfm | 89 ++++++++++++++++++++++++++++++++++++ portal/portal.js | 88 +++++++++++++++++++++++++++++++---- 4 files changed, 243 insertions(+), 8 deletions(-) create mode 100644 api/portal/addTeamMember.cfm create mode 100644 api/portal/searchUser.cfm diff --git a/api/Application.cfm b/api/Application.cfm index 21a94a5..f995725 100644 --- a/api/Application.cfm +++ b/api/Application.cfm @@ -152,6 +152,8 @@ if (len(request._api_path)) { if (findNoCase("/api/portal/stats.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/portal/myBusinesses.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/portal/team.cfm", request._api_path)) request._api_isPublic = true; + if (findNoCase("/api/portal/searchUser.cfm", request._api_path)) request._api_isPublic = true; + if (findNoCase("/api/portal/addTeamMember.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/businesses/setHiring.cfm", request._api_path)) request._api_isPublic = true; // Order history (auth handled in endpoint) diff --git a/api/portal/addTeamMember.cfm b/api/portal/addTeamMember.cfm new file mode 100644 index 0000000..e79f2a3 --- /dev/null +++ b/api/portal/addTeamMember.cfm @@ -0,0 +1,72 @@ + + + + + +function apiAbort(required struct payload) { + writeOutput(serializeJSON(payload)); + abort; +} + +function readJsonBody() { + var raw = getHttpRequestData().content; + if (isNull(raw)) raw = ""; + if (!len(trim(raw))) return {}; + try { + var data = deserializeJSON(raw); + if (isStruct(data)) return data; + } catch (any e) {} + return {}; +} + +data = readJsonBody(); +businessId = structKeyExists(data, "BusinessID") ? val(data.BusinessID) : 0; +userId = structKeyExists(data, "UserID") ? val(data.UserID) : 0; + +if (businessId <= 0) { + apiAbort({ "OK": false, "ERROR": "missing_business_id" }); +} +if (userId <= 0) { + apiAbort({ "OK": false, "ERROR": "missing_user_id" }); +} + +try { + // Check if already exists + qCheck = queryExecute(" + SELECT EmployeeID, EmployeeIsActive FROM lt_Users_Businesses_Employees + WHERE BusinessID = ? AND UserID = ? + ", [ + { value: businessId, cfsqltype: "cf_sql_integer" }, + { value: userId, cfsqltype: "cf_sql_integer" } + ], { datasource: "payfrit" }); + + if (qCheck.recordCount > 0) { + // Update to active + queryExecute(" + UPDATE lt_Users_Businesses_Employees + SET EmployeeIsActive = 1, EmployeeStatusID = 2 + WHERE BusinessID = ? AND UserID = ? + ", [ + { value: businessId, cfsqltype: "cf_sql_integer" }, + { value: userId, cfsqltype: "cf_sql_integer" } + ], { datasource: "payfrit" }); + apiAbort({ "OK": true, "MESSAGE": "Employee reactivated", "EmployeeID": qCheck.EmployeeID }); + } + + // Insert new + queryExecute(" + INSERT INTO lt_Users_Businesses_Employees (BusinessID, UserID, EmployeeStatusID, EmployeeIsActive) + VALUES (?, ?, 2, 1) + ", [ + { value: businessId, cfsqltype: "cf_sql_integer" }, + { value: userId, cfsqltype: "cf_sql_integer" } + ], { datasource: "payfrit" }); + + qNew = queryExecute("SELECT LAST_INSERT_ID() AS EmployeeID", {}, { datasource: "payfrit" }); + + apiAbort({ "OK": true, "MESSAGE": "Team member added", "EmployeeID": qNew.EmployeeID }); + +} catch (any e) { + apiAbort({ "OK": false, "ERROR": "server_error", "MESSAGE": e.message }); +} + diff --git a/api/portal/searchUser.cfm b/api/portal/searchUser.cfm new file mode 100644 index 0000000..2a6d9ed --- /dev/null +++ b/api/portal/searchUser.cfm @@ -0,0 +1,89 @@ + + + + + +function apiAbort(required struct payload) { + writeOutput(serializeJSON(payload)); + abort; +} + +function readJsonBody() { + var raw = getHttpRequestData().content; + if (isNull(raw)) raw = ""; + if (!len(trim(raw))) return {}; + try { + var data = deserializeJSON(raw); + if (isStruct(data)) return data; + } catch (any e) {} + return {}; +} + +// Normalize phone to digits only +function normalizePhone(phone) { + return reReplace(phone, "[^0-9]", "", "all"); +} + +data = readJsonBody(); +query = structKeyExists(data, "Query") ? trim(data.Query) : ""; +businessId = structKeyExists(data, "BusinessID") ? val(data.BusinessID) : 0; + +if (len(query) < 3) { + apiAbort({ "OK": false, "ERROR": "query_too_short", "MESSAGE": "Enter at least 3 characters" }); +} + +try { + // Detect if it's a phone number or email + isPhone = reFind("^[\d\s\-\(\)\+]+$", query) && len(normalizePhone(query)) >= 7; + + if (isPhone) { + // Search by phone - normalize both sides + phoneDigits = normalizePhone(query); + qUser = queryExecute(" + SELECT UserID, UserFirstName, UserLastName, UserContactNumber, UserEmailAddress + FROM Users + WHERE REPLACE(REPLACE(REPLACE(REPLACE(UserContactNumber, '-', ''), ' ', ''), '(', ''), ')', '') LIKE :phone + LIMIT 1 + ", { + phone: { value: "%" & phoneDigits & "%", cfsqltype: "cf_sql_varchar" } + }, { datasource: "payfrit" }); + } else { + // Search by email + qUser = queryExecute(" + SELECT UserID, UserFirstName, UserLastName, UserContactNumber, UserEmailAddress + FROM Users + WHERE UserEmailAddress = :email + LIMIT 1 + ", { + email: { value: query, cfsqltype: "cf_sql_varchar" } + }, { datasource: "payfrit" }); + } + + if (qUser.recordCount > 0) { + // Check if already on team + qTeam = queryExecute(" + SELECT EmployeeID FROM lt_Users_Businesses_Employees + WHERE BusinessID = :bizId AND UserID = :userId + ", { + bizId: { value: businessId, cfsqltype: "cf_sql_integer" }, + userId: { value: qUser.UserID, cfsqltype: "cf_sql_integer" } + }, { datasource: "payfrit" }); + + apiAbort({ + "OK": true, + "USER": { + "UserID": qUser.UserID, + "Name": trim(qUser.UserFirstName & " " & qUser.UserLastName), + "Phone": qUser.UserContactNumber, + "Email": qUser.UserEmailAddress, + "AlreadyOnTeam": qTeam.recordCount > 0 + } + }); + } else { + apiAbort({ "OK": true, "USER": javaCast("null", "") }); + } + +} catch (any e) { + apiAbort({ "OK": false, "ERROR": "server_error", "MESSAGE": e.message }); +} + diff --git a/portal/portal.js b/portal/portal.js index 08693d4..fbe18b3 100644 --- a/portal/portal.js +++ b/portal/portal.js @@ -1249,13 +1249,15 @@ const Portal = { // Show invite modal showInviteModal() { - document.getElementById('modalTitle').textContent = 'Invite Team Member'; + document.getElementById('modalTitle').textContent = 'Add Team Member'; document.getElementById('modalBody').innerHTML = `
- - + + + Enter the person's phone number or email address
+
- +
`; this.showModal(); - document.getElementById('inviteForm').addEventListener('submit', (e) => { + let foundUserId = null; + + document.getElementById('inviteForm').addEventListener('submit', async (e) => { e.preventDefault(); - const email = document.getElementById('inviteEmail').value; - this.toast(`Invitation sent to ${email}`, 'success'); - this.closeModal(); + const contact = document.getElementById('inviteContact').value.trim(); + const btn = document.getElementById('inviteSubmitBtn'); + const resultsDiv = document.getElementById('userSearchResults'); + + if (foundUserId) { + // Actually add the team member + btn.disabled = true; + btn.textContent = 'Adding...'; + try { + const response = await fetch(`${this.config.apiBaseUrl}/portal/addTeamMember.cfm`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + BusinessID: this.config.businessId, + UserID: foundUserId + }) + }); + const data = await response.json(); + if (data.OK) { + this.toast('Team member added!', 'success'); + this.closeModal(); + this.loadTeamPage(); + } else { + this.toast(data.MESSAGE || 'Failed to add', 'error'); + } + } catch (err) { + this.toast('Error adding team member', 'error'); + } + btn.disabled = false; + btn.textContent = 'Add Team Member'; + return; + } + + // Search for user + btn.disabled = true; + btn.textContent = 'Searching...'; + + try { + const response = await fetch(`${this.config.apiBaseUrl}/portal/searchUser.cfm`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ Query: contact, BusinessID: this.config.businessId }) + }); + const data = await response.json(); + + if (data.OK && data.USER) { + foundUserId = data.USER.UserID; + resultsDiv.style.display = 'block'; + resultsDiv.innerHTML = ` +
+ Found: ${data.USER.Name}
+ ${data.USER.Phone || data.USER.Email || ''} +
+ `; + btn.textContent = 'Add Team Member'; + } else { + resultsDiv.style.display = 'block'; + resultsDiv.innerHTML = ` +
+ Not found
+ No user with that phone/email. They need to create an account first. +
+ `; + btn.textContent = 'Search & Add'; + } + } catch (err) { + resultsDiv.style.display = 'block'; + resultsDiv.innerHTML = `
Search failed: ${err.message}
`; + } + + btn.disabled = false; }); },