Add team member search/add with phone or email support

This commit is contained in:
John Mizerek 2026-01-18 13:27:02 -08:00
parent 108a90531e
commit ef92c86fcc
4 changed files with 243 additions and 8 deletions

View file

@ -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)

View file

@ -0,0 +1,72 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
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 });
}
</cfscript>

89
api/portal/searchUser.cfm Normal file
View file

@ -0,0 +1,89 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
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 });
}
</cfscript>

View file

@ -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 = `
<form id="inviteForm" class="form">
<div class="form-group">
<label>Email Address</label>
<input type="email" id="inviteEmail" class="form-input" required>
<label>Phone Number or Email</label>
<input type="text" id="inviteContact" class="form-input" placeholder="(555) 123-4567 or email@example.com" required>
<small style="color: #888; margin-top: 4px; display: block;">Enter the person's phone number or email address</small>
</div>
<div id="userSearchResults" style="margin: 10px 0; display: none;"></div>
<div class="form-group">
<label>Role</label>
<select id="inviteRole" class="form-select">
@ -1264,16 +1266,86 @@ const Portal = {
<option value="admin">Admin</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Send Invitation</button>
<button type="submit" class="btn btn-primary" id="inviteSubmitBtn">Search & Add</button>
</form>
`;
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 = `
<div style="padding: 12px; background: #e8f5e9; border-radius: 8px; border: 1px solid #4caf50;">
<strong style="color: #2e7d32;">Found:</strong> ${data.USER.Name}<br>
<small style="color: #666;">${data.USER.Phone || data.USER.Email || ''}</small>
</div>
`;
btn.textContent = 'Add Team Member';
} else {
resultsDiv.style.display = 'block';
resultsDiv.innerHTML = `
<div style="padding: 12px; background: #fff3e0; border-radius: 8px; border: 1px solid #ff9800;">
<strong style="color: #e65100;">Not found</strong><br>
<small style="color: #666;">No user with that phone/email. They need to create an account first.</small>
</div>
`;
btn.textContent = 'Search & Add';
}
} catch (err) {
resultsDiv.style.display = 'block';
resultsDiv.innerHTML = `<div style="color: red;">Search failed: ${err.message}</div>`;
}
btn.disabled = false;
});
},