Add team member search/add with phone or email support
This commit is contained in:
parent
108a90531e
commit
ef92c86fcc
4 changed files with 243 additions and 8 deletions
|
|
@ -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/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/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/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;
|
if (findNoCase("/api/businesses/setHiring.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
||||||
// Order history (auth handled in endpoint)
|
// Order history (auth handled in endpoint)
|
||||||
|
|
|
||||||
72
api/portal/addTeamMember.cfm
Normal file
72
api/portal/addTeamMember.cfm
Normal 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
89
api/portal/searchUser.cfm
Normal 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>
|
||||||
|
|
@ -1249,13 +1249,15 @@ const Portal = {
|
||||||
|
|
||||||
// Show invite modal
|
// Show invite modal
|
||||||
showInviteModal() {
|
showInviteModal() {
|
||||||
document.getElementById('modalTitle').textContent = 'Invite Team Member';
|
document.getElementById('modalTitle').textContent = 'Add Team Member';
|
||||||
document.getElementById('modalBody').innerHTML = `
|
document.getElementById('modalBody').innerHTML = `
|
||||||
<form id="inviteForm" class="form">
|
<form id="inviteForm" class="form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Email Address</label>
|
<label>Phone Number or Email</label>
|
||||||
<input type="email" id="inviteEmail" class="form-input" required>
|
<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>
|
||||||
|
<div id="userSearchResults" style="margin: 10px 0; display: none;"></div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Role</label>
|
<label>Role</label>
|
||||||
<select id="inviteRole" class="form-select">
|
<select id="inviteRole" class="form-select">
|
||||||
|
|
@ -1264,16 +1266,86 @@ const Portal = {
|
||||||
<option value="admin">Admin</option>
|
<option value="admin">Admin</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Send Invitation</button>
|
<button type="submit" class="btn btn-primary" id="inviteSubmitBtn">Search & Add</button>
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
this.showModal();
|
this.showModal();
|
||||||
|
|
||||||
document.getElementById('inviteForm').addEventListener('submit', (e) => {
|
let foundUserId = null;
|
||||||
|
|
||||||
|
document.getElementById('inviteForm').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const email = document.getElementById('inviteEmail').value;
|
const contact = document.getElementById('inviteContact').value.trim();
|
||||||
this.toast(`Invitation sent to ${email}`, 'success');
|
const btn = document.getElementById('inviteSubmitBtn');
|
||||||
this.closeModal();
|
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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue