Add staff role system: Staff keeps cash, Manager/Admin collect for restaurant
- Create tt_StaffRoles lookup table (Staff, Manager, Admin) - Add RoleID column to Employees table (default: Staff) - Wire portal role dropdown to addTeamMember API - Return RoleName in team list and RoleID to Android - Skip worker payout ledger and cash_debit for Manager/Admin roles on cash task completion (they collect on behalf of the restaurant) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
476d7f9df1
commit
d1910a7d34
6 changed files with 34 additions and 14 deletions
|
|
@ -22,6 +22,8 @@ function readJsonBody() {
|
|||
data = readJsonBody();
|
||||
businessId = structKeyExists(data, "BusinessID") ? val(data.BusinessID) : 0;
|
||||
userId = structKeyExists(data, "UserID") ? val(data.UserID) : 0;
|
||||
roleId = structKeyExists(data, "RoleID") ? val(data.RoleID) : 1;
|
||||
if (roleId < 1 || roleId > 3) roleId = 1;
|
||||
|
||||
if (businessId <= 0) {
|
||||
apiAbort({ "OK": false, "ERROR": "missing_business_id" });
|
||||
|
|
@ -41,12 +43,13 @@ try {
|
|||
], { datasource: "payfrit" });
|
||||
|
||||
if (qCheck.recordCount > 0) {
|
||||
// Update to active
|
||||
// Update to active with role
|
||||
queryTimed("
|
||||
UPDATE Employees
|
||||
SET IsActive = 1, StatusID = 2
|
||||
SET IsActive = 1, StatusID = 2, RoleID = ?
|
||||
WHERE BusinessID = ? AND UserID = ?
|
||||
", [
|
||||
{ value: roleId, cfsqltype: "cf_sql_integer" },
|
||||
{ value: businessId, cfsqltype: "cf_sql_integer" },
|
||||
{ value: userId, cfsqltype: "cf_sql_integer" }
|
||||
], { datasource: "payfrit" });
|
||||
|
|
@ -58,11 +61,12 @@ try {
|
|||
// the business relationship is established via ServicePoint -> Beacon chain.
|
||||
// Kept for legacy/convenience but could be derived from context.
|
||||
queryTimed("
|
||||
INSERT INTO Employees (BusinessID, UserID, StatusID, IsActive)
|
||||
VALUES (?, ?, 2, 1)
|
||||
INSERT INTO Employees (BusinessID, UserID, StatusID, IsActive, RoleID)
|
||||
VALUES (?, ?, 2, 1, ?)
|
||||
", [
|
||||
{ value: businessId, cfsqltype: "cf_sql_integer" },
|
||||
{ value: userId, cfsqltype: "cf_sql_integer" }
|
||||
{ value: userId, cfsqltype: "cf_sql_integer" },
|
||||
{ value: roleId, cfsqltype: "cf_sql_integer" }
|
||||
], { datasource: "payfrit" });
|
||||
|
||||
qNew = queryTimed("SELECT LAST_INSERT_ID() AS EmployeeID", {}, { datasource: "payfrit" });
|
||||
|
|
|
|||
|
|
@ -45,11 +45,13 @@ try {
|
|||
e.ID,
|
||||
e.UserID,
|
||||
e.StatusID,
|
||||
e.RoleID,
|
||||
CAST(e.IsActive AS UNSIGNED) AS IsActive,
|
||||
u.FirstName,
|
||||
u.LastName,
|
||||
u.EmailAddress,
|
||||
u.ContactNumber,
|
||||
COALESCE(sr.Name, 'Staff') AS RoleName,
|
||||
CASE e.StatusID
|
||||
WHEN 0 THEN 'Pending'
|
||||
WHEN 1 THEN 'Invited'
|
||||
|
|
@ -59,6 +61,7 @@ try {
|
|||
END AS StatusName
|
||||
FROM Employees e
|
||||
JOIN Users u ON e.UserID = u.ID
|
||||
LEFT JOIN tt_StaffRoles sr ON sr.ID = e.RoleID
|
||||
WHERE e.BusinessID = ?
|
||||
ORDER BY e.IsActive DESC, u.FirstName ASC
|
||||
", [
|
||||
|
|
@ -75,6 +78,8 @@ try {
|
|||
"LastName": row.LastName,
|
||||
"Email": row.EmailAddress,
|
||||
"Phone": row.ContactNumber,
|
||||
"RoleID": row.RoleID,
|
||||
"RoleName": row.RoleName,
|
||||
"StatusID": row.StatusID,
|
||||
"StatusName": row.StatusName,
|
||||
"IsActive": val(row.IsActive) == 1
|
||||
|
|
|
|||
|
|
@ -50,11 +50,13 @@
|
|||
SELECT t.ID, t.ClaimedByUserID, t.CompletedOn, t.OrderID, t.TaskTypeID, t.BusinessID,
|
||||
o.UserID AS CustomerUserID, o.ServicePointID,
|
||||
tt.Name AS TaskTypeName,
|
||||
b.UserID AS BusinessOwnerUserID
|
||||
b.UserID AS BusinessOwnerUserID,
|
||||
COALESCE(emp.RoleID, 1) AS WorkerRoleID
|
||||
FROM Tasks t
|
||||
LEFT JOIN Orders o ON o.ID = t.OrderID
|
||||
LEFT JOIN tt_TaskTypes tt ON tt.ID = t.TaskTypeID
|
||||
LEFT JOIN Businesses b ON b.ID = t.BusinessID
|
||||
LEFT JOIN Employees emp ON emp.BusinessID = t.BusinessID AND emp.UserID = t.ClaimedByUserID AND emp.IsActive = 1
|
||||
WHERE t.ID = ?
|
||||
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||
|
||||
|
|
@ -65,6 +67,8 @@
|
|||
<!--- Chat tasks (TaskTypeID = 2) can be closed without being claimed first --->
|
||||
<cfset isChatTask = (qTask.TaskTypeID EQ 2)>
|
||||
<cfset isCashTask = (len(trim(qTask.TaskTypeName)) GT 0 AND findNoCase("Cash", qTask.TaskTypeName) GT 0)>
|
||||
<!--- Manager (2) and Admin (3) collect cash on behalf of the restaurant; Staff (1) keeps cash as payout --->
|
||||
<cfset isAdminRole = (val(qTask.WorkerRoleID) GTE 2)>
|
||||
|
||||
<cfif (NOT isChatTask) AND (qTask.ClaimedByUserID EQ 0)>
|
||||
<cfset apiAbort({ "OK": false, "ERROR": "not_claimed", "MESSAGE": "Task has not been claimed yet." })>
|
||||
|
|
@ -221,9 +225,10 @@
|
|||
</cfif>
|
||||
|
||||
<!--- === PAYOUT LEDGER + ACTIVATION WITHHOLDING === --->
|
||||
<!--- Skip for admin/manager roles — they aren't earning personal task pay --->
|
||||
<cfset ledgerCreated = false>
|
||||
<cfset workerUserID_for_payout = val(qTask.ClaimedByUserID)>
|
||||
<cfif workerUserID_for_payout GT 0>
|
||||
<cfif workerUserID_for_payout GT 0 AND NOT isAdminRole>
|
||||
<!--- Get PayCents from the task --->
|
||||
<cfset qTaskPay = queryTimed("
|
||||
SELECT PayCents FROM Tasks WHERE ID = ?
|
||||
|
|
@ -301,7 +306,8 @@
|
|||
</cfif>
|
||||
|
||||
<!--- Debit worker (received physical cash, debit digitally) --->
|
||||
<cfif workerUserID_for_payout GT 0>
|
||||
<!--- Skip for admin/manager — they collected cash on behalf of the restaurant --->
|
||||
<cfif workerUserID_for_payout GT 0 AND NOT isAdminRole>
|
||||
<cfset queryTimed("
|
||||
INSERT INTO WorkPayoutLedgers
|
||||
(UserID, TaskID, GrossEarningsCents, ActivationWithheldCents, NetTransferCents, Status)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
e.BusinessID,
|
||||
MIN(e.StatusID) AS StatusID,
|
||||
MAX(e.IsActive) AS IsActive,
|
||||
MAX(e.RoleID) AS RoleID,
|
||||
b.Name AS Name,
|
||||
(SELECT COUNT(*) FROM Tasks t WHERE t.BusinessID = e.BusinessID AND t.ClaimedByUserID = 0 AND t.CompletedOn IS NULL) AS PendingTaskCount,
|
||||
(SELECT COUNT(*) FROM Tasks t WHERE t.BusinessID = e.BusinessID AND t.ClaimedByUserID = ? AND t.CompletedOn IS NULL) AS ActiveTaskCount
|
||||
|
|
@ -60,6 +61,7 @@
|
|||
"Address": "",
|
||||
"City": "",
|
||||
"StatusID": qBusinesses.StatusID,
|
||||
"RoleID": qBusinesses.RoleID,
|
||||
"PendingTaskCount": qBusinesses.PendingTaskCount,
|
||||
"ActiveTaskCount": qBusinesses.ActiveTaskCount
|
||||
})>
|
||||
|
|
|
|||
|
|
@ -372,12 +372,13 @@
|
|||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="teamTableBody">
|
||||
<tr><td colspan="5" class="empty-state">Loading team...</td></tr>
|
||||
<tr><td colspan="6" class="empty-state">Loading team...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -625,7 +625,7 @@ const Portal = {
|
|||
async loadTeam() {
|
||||
console.log('[Portal] Loading team for business:', this.config.businessId);
|
||||
const tbody = document.getElementById('teamTableBody');
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Loading team...</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">Loading team...</td></tr>';
|
||||
|
||||
// Load hiring toggle state from business data
|
||||
this.loadHiringToggle();
|
||||
|
|
@ -643,7 +643,7 @@ const Portal = {
|
|||
|
||||
if (data.OK && data.TEAM) {
|
||||
if (data.TEAM.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No team members yet</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">No team members yet</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -657,6 +657,7 @@ const Portal = {
|
|||
</td>
|
||||
<td>${member.Email || '-'}</td>
|
||||
<td>${this.formatPhone(member.Phone)}</td>
|
||||
<td>${member.RoleName || 'Staff'}</td>
|
||||
<td>
|
||||
<span class="status-badge ${this.getStatusClass(member.StatusID)}">
|
||||
${member.StatusName || 'Unknown'}
|
||||
|
|
@ -668,11 +669,11 @@ const Portal = {
|
|||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Failed to load team</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">Failed to load team</td></tr>';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Portal] Error loading team:', err);
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Error loading team</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">Error loading team</td></tr>';
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -1604,7 +1605,8 @@ const Portal = {
|
|||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
BusinessID: this.config.businessId,
|
||||
UserID: foundUserId
|
||||
UserID: foundUserId,
|
||||
RoleID: { staff: 1, manager: 2, admin: 3 }[document.getElementById('inviteRole').value] || 1
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
|
|
|
|||
Reference in a new issue