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/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)
|
||||
|
|
|
|||
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
|
||||
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');
|
||||
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;
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue