Add team endpoint and chat features for portal
- Add /api/portal/team.cfm for employee listing - Add chat endpoints (getMessages, sendMessage, markRead, getActiveChat) - Add OTP authentication endpoints - Add address management endpoints (delete, setDefault, states) - Add task completion and chat task endpoints - Update Application.cfm allowlist Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d8d7efe056
commit
8092384702
35 changed files with 2219 additions and 38 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
<cfsetting enablecfoutputonly="true">
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
|
||||||
<!---
|
<!---
|
||||||
Payfrit API Application.cfm
|
Payfrit API Application.cfm (updated)
|
||||||
|
|
||||||
FIX: Provide a DEFAULT datasource so endpoints can call queryExecute()
|
FIX: Provide a DEFAULT datasource so endpoints can call queryExecute()
|
||||||
without specifying { datasource="payfrit" } every time.
|
without specifying { datasource="payfrit" } every time.
|
||||||
|
|
@ -32,6 +32,11 @@
|
||||||
showdebugoutput="false"
|
showdebugoutput="false"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<!--- Initialize Twilio for SMS --->
|
||||||
|
<cfif NOT structKeyExists(application, "twilioObj")>
|
||||||
|
<cfset application.twilioObj = new library.cfc.twilio() />
|
||||||
|
</cfif>
|
||||||
|
|
||||||
<!--- Stripe Configuration (loads from config/stripe.cfm) --->
|
<!--- Stripe Configuration (loads from config/stripe.cfm) --->
|
||||||
<cfinclude template="config/stripe.cfm">
|
<cfinclude template="config/stripe.cfm">
|
||||||
|
|
||||||
|
|
@ -73,6 +78,11 @@ if (len(request._api_path)) {
|
||||||
if (findNoCase("/api/auth/login.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/auth/login.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/auth/logout.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/auth/logout.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/auth/avatar.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/auth/avatar.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/auth/sendOTP.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/auth/verifyOTP.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/auth/loginOTP.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/auth/verifyLoginOTP.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/auth/completeProfile.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
||||||
if (findNoCase("/api/businesses/list.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/businesses/list.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/businesses/get.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/businesses/get.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
@ -80,6 +90,8 @@ if (len(request._api_path)) {
|
||||||
if (findNoCase("/api/beacons/list_all.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/beacons/list_all.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/beacons/getBusinessFromBeacon.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/beacons/getBusinessFromBeacon.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/menu/items.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/menu/items.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/addresses/states.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/debug/", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/orders/getOrCreateCart.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/orders/getOrCreateCart.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/orders/getCart.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/orders/getCart.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/orders/setLineItem.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/orders/setLineItem.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
@ -89,11 +101,25 @@ if (len(request._api_path)) {
|
||||||
if (findNoCase("/api/orders/checkStatusUpdate.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/orders/checkStatusUpdate.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/orders/getDetail.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/orders/getDetail.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
||||||
|
if (findNoCase("/api/users/search.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
||||||
if (findNoCase("/api/tasks/listPending.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/tasks/listPending.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/tasks/accept.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/tasks/accept.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/tasks/listMine.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/tasks/listMine.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/tasks/complete.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/tasks/complete.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/tasks/completeChat.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/tasks/getDetails.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/tasks/getDetails.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/tasks/callserver", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/tasks/createChat.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
||||||
|
// Chat endpoints
|
||||||
|
if (findNoCase("/api/chat/getMessages.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/chat/sendMessage.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/chat/markRead.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/chat/getActiveChat.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
||||||
|
// Token validation (for WebSocket server)
|
||||||
|
if (findNoCase("/api/auth/validateToken.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
||||||
// Worker app endpoints
|
// Worker app endpoints
|
||||||
if (findNoCase("/api/workers/myBusinesses.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/workers/myBusinesses.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
@ -101,6 +127,7 @@ if (len(request._api_path)) {
|
||||||
// Portal endpoints
|
// Portal endpoints
|
||||||
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;
|
||||||
|
|
||||||
// Order history (auth handled in endpoint)
|
// Order history (auth handled in endpoint)
|
||||||
if (findNoCase("/api/orders/history.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/orders/history.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
@ -121,6 +148,7 @@ if (len(request._api_path)) {
|
||||||
// Admin endpoints (protected by localhost check in each file)
|
// Admin endpoints (protected by localhost check in each file)
|
||||||
if (findNoCase("/api/admin/resetTestData.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/admin/resetTestData.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/admin/debugTasks.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/admin/debugTasks.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/admin/testTaskType.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/admin/testTaskInsert.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/admin/testTaskInsert.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/admin/debugBusinesses.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/admin/debugBusinesses.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/admin/setupStations.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/admin/setupStations.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
@ -149,6 +177,11 @@ if (len(request._api_path)) {
|
||||||
if (findNoCase("/api/admin/copyDrinksToBigDeans.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/admin/copyDrinksToBigDeans.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/admin/debugDrinkStructure.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/admin/debugDrinkStructure.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/admin/addDrinkModifiers.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/admin/addDrinkModifiers.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/admin/add_task_columns.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/admin/add_service_category.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/admin/createChatMessagesTable.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/admin/addTaskSourceColumns.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/admin/debugChatMessages.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
||||||
// Setup/Import endpoints
|
// Setup/Import endpoints
|
||||||
if (findNoCase("/api/setup/importBusiness.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/setup/importBusiness.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ try {
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
// Get state info for response
|
// Get state info for response
|
||||||
qState = queryExecute("SELECT StateAbbreviation, StateName FROM States WHERE StateID = :stateId", {
|
qState = queryExecute("SELECT tt_StateAbbreviation as StateAbbreviation, tt_StateName as StateName FROM tt_States WHERE tt_StateID = :stateId", {
|
||||||
stateId: { value: stateId, cfsqltype: "cf_sql_integer" }
|
stateId: { value: stateId, cfsqltype: "cf_sql_integer" }
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
|
|
||||||
103
api/addresses/delete.cfm
Normal file
103
api/addresses/delete.cfm
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8">
|
||||||
|
|
||||||
|
<!--- Soft-delete a delivery address for the authenticated user --->
|
||||||
|
<cfscript>
|
||||||
|
function readJsonBody() {
|
||||||
|
var raw = getHttpRequestData().content;
|
||||||
|
if (isNull(raw) || len(trim(toString(raw))) == 0) return {};
|
||||||
|
try {
|
||||||
|
var data = deserializeJSON(toString(raw));
|
||||||
|
return isStruct(data) ? data : {};
|
||||||
|
} catch (any e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
userId = request.UserID ?: 0;
|
||||||
|
|
||||||
|
if (userId <= 0) {
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "unauthorized",
|
||||||
|
"MESSAGE": "Authentication required"
|
||||||
|
}));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = readJsonBody();
|
||||||
|
addressId = val(data.AddressID ?: 0);
|
||||||
|
|
||||||
|
if (addressId <= 0) {
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "missing_field",
|
||||||
|
"MESSAGE": "AddressID is required"
|
||||||
|
}));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify address belongs to user
|
||||||
|
qCheck = queryExecute("
|
||||||
|
SELECT AddressID, AddressIsDefaultDelivery
|
||||||
|
FROM Addresses
|
||||||
|
WHERE AddressID = :addressId
|
||||||
|
AND AddressUserID = :userId
|
||||||
|
AND AddressIsDeleted = 0
|
||||||
|
", {
|
||||||
|
addressId: { value: addressId, cfsqltype: "cf_sql_integer" },
|
||||||
|
userId: { value: userId, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qCheck.recordCount == 0) {
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "not_found",
|
||||||
|
"MESSAGE": "Address not found"
|
||||||
|
}));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
wasDefault = qCheck.AddressIsDefaultDelivery == 1;
|
||||||
|
|
||||||
|
// Soft delete the address
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Addresses
|
||||||
|
SET AddressIsDeleted = 1,
|
||||||
|
AddressIsDefaultDelivery = 0
|
||||||
|
WHERE AddressID = :addressId
|
||||||
|
", {
|
||||||
|
addressId: { value: addressId, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// If this was the default, set another one as default
|
||||||
|
if (wasDefault) {
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Addresses
|
||||||
|
SET AddressIsDefaultDelivery = 1
|
||||||
|
WHERE AddressUserID = :userId
|
||||||
|
AND (AddressBusinessID = 0 OR AddressBusinessID IS NULL)
|
||||||
|
AND AddressTypeID LIKE '%2%'
|
||||||
|
AND AddressIsDeleted = 0
|
||||||
|
ORDER BY AddressID DESC
|
||||||
|
LIMIT 1
|
||||||
|
", {
|
||||||
|
userId: { value: userId, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"MESSAGE": "Address deleted"
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
|
|
@ -18,25 +18,27 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's delivery addresses (AddressTypeID contains "2" for delivery, BusinessID is 0 or NULL for personal)
|
// Get user's delivery addresses (AddressTypeID contains "2" for delivery, BusinessID is 0 or NULL for personal)
|
||||||
|
// Use GROUP BY to return only distinct addresses based on content
|
||||||
qAddresses = queryExecute("
|
qAddresses = queryExecute("
|
||||||
SELECT
|
SELECT
|
||||||
a.AddressID,
|
MIN(a.AddressID) as AddressID,
|
||||||
a.AddressLabel,
|
a.AddressLabel,
|
||||||
a.AddressIsDefaultDelivery,
|
MAX(a.AddressIsDefaultDelivery) as AddressIsDefaultDelivery,
|
||||||
a.AddressLine1,
|
a.AddressLine1,
|
||||||
a.AddressLine2,
|
a.AddressLine2,
|
||||||
a.AddressCity,
|
a.AddressCity,
|
||||||
a.AddressStateID,
|
a.AddressStateID,
|
||||||
s.StateAbbreviation,
|
s.tt_StateAbbreviation as StateAbbreviation,
|
||||||
s.StateName,
|
s.tt_StateName as StateName,
|
||||||
a.AddressZIPCode
|
a.AddressZIPCode
|
||||||
FROM Addresses a
|
FROM Addresses a
|
||||||
LEFT JOIN States s ON a.AddressStateID = s.StateID
|
LEFT JOIN tt_States s ON a.AddressStateID = s.tt_StateID
|
||||||
WHERE a.AddressUserID = :userId
|
WHERE a.AddressUserID = :userId
|
||||||
AND (a.AddressBusinessID = 0 OR a.AddressBusinessID IS NULL)
|
AND (a.AddressBusinessID = 0 OR a.AddressBusinessID IS NULL)
|
||||||
AND a.AddressTypeID LIKE '%2%'
|
AND a.AddressTypeID LIKE '%2%'
|
||||||
AND a.AddressIsDeleted = 0
|
AND a.AddressIsDeleted = 0
|
||||||
ORDER BY a.AddressIsDefaultDelivery DESC, a.AddressID DESC
|
GROUP BY a.AddressLine1, COALESCE(a.AddressLine2, ''), a.AddressCity, a.AddressStateID, a.AddressZIPCode
|
||||||
|
ORDER BY AddressIsDefaultDelivery DESC, AddressID DESC
|
||||||
", {
|
", {
|
||||||
userId: { value: userId, cfsqltype: "cf_sql_integer" }
|
userId: { value: userId, cfsqltype: "cf_sql_integer" }
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
|
|
|
||||||
95
api/addresses/setDefault.cfm
Normal file
95
api/addresses/setDefault.cfm
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8">
|
||||||
|
|
||||||
|
<!--- Set an address as the default delivery address --->
|
||||||
|
<cfscript>
|
||||||
|
function readJsonBody() {
|
||||||
|
var raw = getHttpRequestData().content;
|
||||||
|
if (isNull(raw) || len(trim(toString(raw))) == 0) return {};
|
||||||
|
try {
|
||||||
|
var data = deserializeJSON(toString(raw));
|
||||||
|
return isStruct(data) ? data : {};
|
||||||
|
} catch (any e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
userId = request.UserID ?: 0;
|
||||||
|
|
||||||
|
if (userId <= 0) {
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "unauthorized",
|
||||||
|
"MESSAGE": "Authentication required"
|
||||||
|
}));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = readJsonBody();
|
||||||
|
addressId = val(data.AddressID ?: 0);
|
||||||
|
|
||||||
|
if (addressId <= 0) {
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "missing_field",
|
||||||
|
"MESSAGE": "AddressID is required"
|
||||||
|
}));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify address belongs to user
|
||||||
|
qCheck = queryExecute("
|
||||||
|
SELECT AddressID
|
||||||
|
FROM Addresses
|
||||||
|
WHERE AddressID = :addressId
|
||||||
|
AND AddressUserID = :userId
|
||||||
|
AND AddressIsDeleted = 0
|
||||||
|
", {
|
||||||
|
addressId: { value: addressId, cfsqltype: "cf_sql_integer" },
|
||||||
|
userId: { value: userId, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qCheck.recordCount == 0) {
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "not_found",
|
||||||
|
"MESSAGE": "Address not found"
|
||||||
|
}));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all defaults for this user
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Addresses
|
||||||
|
SET AddressIsDefaultDelivery = 0
|
||||||
|
WHERE AddressUserID = :userId
|
||||||
|
AND (AddressBusinessID = 0 OR AddressBusinessID IS NULL)
|
||||||
|
AND AddressTypeID LIKE '%2%'
|
||||||
|
", {
|
||||||
|
userId: { value: userId, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Set this one as default
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Addresses
|
||||||
|
SET AddressIsDefaultDelivery = 1
|
||||||
|
WHERE AddressID = :addressId
|
||||||
|
", {
|
||||||
|
addressId: { value: addressId, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"MESSAGE": "Default address updated"
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
35
api/addresses/states.cfm
Normal file
35
api/addresses/states.cfm
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8">
|
||||||
|
|
||||||
|
<!--- List US states for address forms --->
|
||||||
|
<cfscript>
|
||||||
|
try {
|
||||||
|
qStates = queryExecute("
|
||||||
|
SELECT tt_StateID as StateID, tt_StateAbbreviation as StateAbbreviation, tt_StateName as StateName
|
||||||
|
FROM tt_States
|
||||||
|
ORDER BY tt_StateName
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
states = [];
|
||||||
|
for (row in qStates) {
|
||||||
|
arrayAppend(states, {
|
||||||
|
"StateID": row.StateID,
|
||||||
|
"Abbr": row.StateAbbreviation,
|
||||||
|
"Name": row.StateName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"STATES": states
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
66
api/admin/addTaskSourceColumns.cfm
Normal file
66
api/admin/addTaskSourceColumns.cfm
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Add TaskSourceType and TaskSourceID columns to Tasks table
|
||||||
|
// These are needed for chat persistence feature
|
||||||
|
|
||||||
|
result = { "OK": true, "STEPS": [] };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if columns already exist
|
||||||
|
cols = queryExecute("
|
||||||
|
SELECT COLUMN_NAME
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = 'payfrit'
|
||||||
|
AND TABLE_NAME = 'Tasks'
|
||||||
|
AND COLUMN_NAME IN ('TaskSourceType', 'TaskSourceID')
|
||||||
|
", [], { datasource: "payfrit" });
|
||||||
|
|
||||||
|
existingCols = valueList(cols.COLUMN_NAME);
|
||||||
|
arrayAppend(result.STEPS, "Existing columns: #existingCols#");
|
||||||
|
|
||||||
|
// Add TaskSourceType if missing
|
||||||
|
if (!listFindNoCase(existingCols, "TaskSourceType")) {
|
||||||
|
queryExecute("
|
||||||
|
ALTER TABLE Tasks ADD COLUMN TaskSourceType VARCHAR(50) NULL
|
||||||
|
", [], { datasource: "payfrit" });
|
||||||
|
arrayAppend(result.STEPS, "Added TaskSourceType column");
|
||||||
|
} else {
|
||||||
|
arrayAppend(result.STEPS, "TaskSourceType already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add TaskSourceID if missing
|
||||||
|
if (!listFindNoCase(existingCols, "TaskSourceID")) {
|
||||||
|
queryExecute("
|
||||||
|
ALTER TABLE Tasks ADD COLUMN TaskSourceID INT NULL
|
||||||
|
", [], { datasource: "payfrit" });
|
||||||
|
arrayAppend(result.STEPS, "Added TaskSourceID column");
|
||||||
|
} else {
|
||||||
|
arrayAppend(result.STEPS, "TaskSourceID already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify columns now exist
|
||||||
|
verifyQuery = queryExecute("
|
||||||
|
SELECT COLUMN_NAME, DATA_TYPE
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = 'payfrit'
|
||||||
|
AND TABLE_NAME = 'Tasks'
|
||||||
|
AND COLUMN_NAME IN ('TaskSourceType', 'TaskSourceID')
|
||||||
|
", [], { datasource: "payfrit" });
|
||||||
|
|
||||||
|
result.COLUMNS = [];
|
||||||
|
for (row in verifyQuery) {
|
||||||
|
arrayAppend(result.COLUMNS, { "name": row.COLUMN_NAME, "type": row.DATA_TYPE });
|
||||||
|
}
|
||||||
|
|
||||||
|
arrayAppend(result.STEPS, "Migration complete");
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
result.OK = false;
|
||||||
|
result.ERROR = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(result));
|
||||||
|
</cfscript>
|
||||||
53
api/admin/createChatMessagesTable.cfm
Normal file
53
api/admin/createChatMessagesTable.cfm
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfcontent type="application/json; charset=utf-8">
|
||||||
|
<cfscript>
|
||||||
|
try {
|
||||||
|
// Create ChatMessages table
|
||||||
|
queryExecute("
|
||||||
|
CREATE TABLE IF NOT EXISTS ChatMessages (
|
||||||
|
MessageID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
TaskID INT NOT NULL,
|
||||||
|
SenderUserID INT NOT NULL,
|
||||||
|
SenderType ENUM('customer', 'worker') NOT NULL,
|
||||||
|
MessageText TEXT NOT NULL,
|
||||||
|
IsRead TINYINT(1) DEFAULT 0,
|
||||||
|
CreatedOn DATETIME DEFAULT NOW(),
|
||||||
|
|
||||||
|
INDEX idx_task (TaskID),
|
||||||
|
INDEX idx_sender (SenderUserID),
|
||||||
|
INDEX idx_created (CreatedOn)
|
||||||
|
)
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Also add a "Chat" category if it doesn't exist for business 17
|
||||||
|
existing = queryExecute("
|
||||||
|
SELECT TaskCategoryID FROM TaskCategories
|
||||||
|
WHERE TaskCategoryBusinessID = 17 AND TaskCategoryName = 'Chat'
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (existing.recordCount == 0) {
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO TaskCategories (TaskCategoryBusinessID, TaskCategoryName, TaskCategoryColor)
|
||||||
|
VALUES (17, 'Chat', '##2196F3')
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify table was created
|
||||||
|
cols = queryExecute("DESCRIBE ChatMessages", {}, { datasource: "payfrit" });
|
||||||
|
colNames = [];
|
||||||
|
for (c in cols) {
|
||||||
|
arrayAppend(colNames, c.Field);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"MESSAGE": "ChatMessages table created successfully",
|
||||||
|
"COLUMNS": colNames
|
||||||
|
}));
|
||||||
|
} catch (any e) {
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": e.message
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
41
api/admin/debugChatMessages.cfm
Normal file
41
api/admin/debugChatMessages.cfm
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Debug: check ChatMessages table contents
|
||||||
|
try {
|
||||||
|
qAll = queryExecute("SELECT * FROM ChatMessages ORDER BY CreatedOn DESC LIMIT 50", [], { datasource: "payfrit" });
|
||||||
|
|
||||||
|
messages = [];
|
||||||
|
for (row in qAll) {
|
||||||
|
arrayAppend(messages, {
|
||||||
|
"MessageID": row.MessageID,
|
||||||
|
"TaskID": row.TaskID,
|
||||||
|
"SenderUserID": row.SenderUserID,
|
||||||
|
"SenderType": row.SenderType,
|
||||||
|
"MessageText": left(row.MessageText, 100),
|
||||||
|
"CreatedOn": row.CreatedOn
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check schema
|
||||||
|
schema = queryExecute("DESCRIBE ChatMessages", [], { datasource: "payfrit" });
|
||||||
|
cols = [];
|
||||||
|
for (col in schema) {
|
||||||
|
arrayAppend(cols, { "Field": col.Field, "Type": col.Type });
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"TOTAL_MESSAGES": qAll.recordCount,
|
||||||
|
"MESSAGES": messages,
|
||||||
|
"SCHEMA": cols
|
||||||
|
}));
|
||||||
|
} catch (any e) {
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": e.message
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
17
api/admin/testTaskType.cfm
Normal file
17
api/admin/testTaskType.cfm
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfscript>
|
||||||
|
qTask = queryExecute("
|
||||||
|
SELECT TaskID, TaskTypeID, TaskClaimedByUserID, TaskCompletedOn
|
||||||
|
FROM Tasks
|
||||||
|
WHERE TaskID = 57
|
||||||
|
", [], { datasource: "payfrit" });
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"TaskID": qTask.TaskID,
|
||||||
|
"TaskTypeID": qTask.TaskTypeID,
|
||||||
|
"TaskClaimedByUserID": qTask.TaskClaimedByUserID,
|
||||||
|
"TaskCompletedOn": qTask.TaskCompletedOn
|
||||||
|
}));
|
||||||
|
</cfscript>
|
||||||
143
api/auth/completeProfile.cfm
Normal file
143
api/auth/completeProfile.cfm
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Complete user profile after phone verification
|
||||||
|
*
|
||||||
|
* POST: {
|
||||||
|
* "firstName": "John",
|
||||||
|
* "lastName": "Smith",
|
||||||
|
* "email": "john@example.com"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Requires auth token in header: X-User-Token: <token>
|
||||||
|
* (Parsed by Application.cfm into request.UserID)
|
||||||
|
*
|
||||||
|
* Returns: { OK: true } and sends confirmation email
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthUserId() {
|
||||||
|
// Use request.UserID set by Application.cfm from X-User-Token header
|
||||||
|
if (structKeyExists(request, "UserID") && isNumeric(request.UserID) && request.UserID > 0) {
|
||||||
|
return request.UserID;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidEmail(required string email) {
|
||||||
|
return reFind("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", arguments.email) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
userId = getAuthUserId();
|
||||||
|
if (userId == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "unauthorized", "MESSAGE": "Authentication required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
data = readJsonBody();
|
||||||
|
firstName = structKeyExists(data, "firstName") ? trim(data.firstName) : "";
|
||||||
|
lastName = structKeyExists(data, "lastName") ? trim(data.lastName) : "";
|
||||||
|
email = structKeyExists(data, "email") ? trim(lCase(data.email)) : "";
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!len(firstName)) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_first_name", "MESSAGE": "First name is required" });
|
||||||
|
}
|
||||||
|
if (!len(lastName)) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_last_name", "MESSAGE": "Last name is required" });
|
||||||
|
}
|
||||||
|
if (!len(email) || !isValidEmail(email)) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "invalid_email", "MESSAGE": "Please enter a valid email address" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if email is already used by another verified account
|
||||||
|
qEmailCheck = queryExecute("
|
||||||
|
SELECT UserID FROM Users
|
||||||
|
WHERE UserEmailAddress = :email
|
||||||
|
AND UserIsEmailVerified = 1
|
||||||
|
AND UserID != :userId
|
||||||
|
LIMIT 1
|
||||||
|
", {
|
||||||
|
email: { value: email, cfsqltype: "cf_sql_varchar" },
|
||||||
|
userId: { value: userId, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qEmailCheck.recordCount > 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "email_exists", "MESSAGE": "This email is already associated with another account" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current user UUID for email confirmation link
|
||||||
|
qUser = queryExecute("
|
||||||
|
SELECT UserUUID FROM Users WHERE UserID = :userId
|
||||||
|
", { userId: { value: userId, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Update user profile AND mark account as verified/active
|
||||||
|
// This completes the signup process
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Users
|
||||||
|
SET UserFirstName = :firstName,
|
||||||
|
UserLastName = :lastName,
|
||||||
|
UserEmailAddress = :email,
|
||||||
|
UserIsEmailVerified = 0,
|
||||||
|
UserIsContactVerified = 1,
|
||||||
|
UserIsActive = 1
|
||||||
|
WHERE UserID = :userId
|
||||||
|
", {
|
||||||
|
firstName: { value: firstName, cfsqltype: "cf_sql_varchar" },
|
||||||
|
lastName: { value: lastName, cfsqltype: "cf_sql_varchar" },
|
||||||
|
email: { value: email, cfsqltype: "cf_sql_varchar" },
|
||||||
|
userId: { value: userId, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Send confirmation email
|
||||||
|
confirmLink = "https://biz.payfrit.com/confirm_email.cfm?UUID=" & qUser.UserUUID;
|
||||||
|
emailBody = "
|
||||||
|
<p>Welcome to Payfrit, #firstName#!</p>
|
||||||
|
<p>Please click the link below to confirm your email address:</p>
|
||||||
|
<p><a href='#confirmLink#'>Confirm Email</a></p>
|
||||||
|
<p>Or copy and paste this URL into your browser:</p>
|
||||||
|
<p>#confirmLink#</p>
|
||||||
|
<p>Thanks,<br>The Payfrit Team</p>
|
||||||
|
";
|
||||||
|
</cfscript>
|
||||||
|
|
||||||
|
<cfmail to="#email#"
|
||||||
|
from="admin@payfrit.com"
|
||||||
|
subject="Welcome to Payfrit - Please confirm your email"
|
||||||
|
type="html">
|
||||||
|
<cfoutput>#emailBody#</cfoutput>
|
||||||
|
</cfmail>
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"MESSAGE": "Profile updated. Please check your email to confirm your address."
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
97
api/auth/loginOTP.cfm
Normal file
97
api/auth/loginOTP.cfm
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Send OTP to phone number for LOGIN (existing verified accounts)
|
||||||
|
*
|
||||||
|
* POST: { "phone": "5551234567" }
|
||||||
|
*
|
||||||
|
* Returns: { OK: true, UUID: "..." } or { OK: false, ERROR: "..." }
|
||||||
|
*
|
||||||
|
* Only works for verified accounts. Returns error if no account found.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePhone(required string p) {
|
||||||
|
var x = trim(arguments.p);
|
||||||
|
x = reReplace(x, "[^0-9]", "", "all");
|
||||||
|
if (len(x) == 11 && left(x, 1) == "1") {
|
||||||
|
x = right(x, 10);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateOTP() {
|
||||||
|
return randRange(100000, 999999);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
phone = structKeyExists(data, "phone") ? normalizePhone(data.phone) : "";
|
||||||
|
|
||||||
|
if (len(phone) != 10) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "invalid_phone", "MESSAGE": "Please enter a valid 10-digit phone number" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find verified account with this phone
|
||||||
|
qUser = queryExecute("
|
||||||
|
SELECT UserID, UserUUID
|
||||||
|
FROM Users
|
||||||
|
WHERE UserContactNumber = :phone
|
||||||
|
AND UserIsContactVerified = 1
|
||||||
|
LIMIT 1
|
||||||
|
", { phone: { value: phone, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qUser.recordCount == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "no_account", "MESSAGE": "No account found with this phone number. Please sign up first." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate and save OTP
|
||||||
|
otp = generateOTP();
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Users
|
||||||
|
SET UserMobileVerifyCode = :otp
|
||||||
|
WHERE UserID = :userId
|
||||||
|
", {
|
||||||
|
otp: { value: otp, cfsqltype: "cf_sql_varchar" },
|
||||||
|
userId: { value: qUser.UserID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Send OTP via Twilio
|
||||||
|
smsResult = application.twilioObj.sendSMS(
|
||||||
|
recipientNumber: "+1" & phone,
|
||||||
|
messageBody: "Your Payfrit login code is: " & otp
|
||||||
|
);
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"UUID": qUser.UserUUID,
|
||||||
|
"MESSAGE": smsResult.success ? "Login code sent" : "SMS failed - please try again"
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
155
api/auth/sendOTP.cfm
Normal file
155
api/auth/sendOTP.cfm
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Send OTP to phone number for signup
|
||||||
|
*
|
||||||
|
* POST: { "phone": "5551234567" }
|
||||||
|
*
|
||||||
|
* Returns: { OK: true, UUID: "..." } or { OK: false, ERROR: "..." }
|
||||||
|
*
|
||||||
|
* If phone already has verified account, returns error.
|
||||||
|
* If phone has unverified account, resends OTP.
|
||||||
|
* Otherwise creates new user record with OTP.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePhone(required string p) {
|
||||||
|
var x = trim(arguments.p);
|
||||||
|
x = reReplace(x, "[^0-9]", "", "all");
|
||||||
|
// Remove leading 1 if 11 digits
|
||||||
|
if (len(x) == 11 && left(x, 1) == "1") {
|
||||||
|
x = right(x, 10);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateOTP() {
|
||||||
|
return randRange(100000, 999999);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
phone = structKeyExists(data, "phone") ? normalizePhone(data.phone) : "";
|
||||||
|
|
||||||
|
if (len(phone) != 10) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "invalid_phone", "MESSAGE": "Please enter a valid 10-digit phone number" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if phone already has a COMPLETE account (verified AND has profile info)
|
||||||
|
// An account is only "complete" if they have a first name (meaning they finished signup)
|
||||||
|
qExisting = queryExecute("
|
||||||
|
SELECT UserID, UserUUID, UserFirstName
|
||||||
|
FROM Users
|
||||||
|
WHERE UserContactNumber = :phone
|
||||||
|
AND UserIsContactVerified > 0
|
||||||
|
AND UserFirstName IS NOT NULL
|
||||||
|
AND LENGTH(TRIM(UserFirstName)) > 0
|
||||||
|
LIMIT 1
|
||||||
|
", { phone: { value: phone, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qExisting.recordCount > 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "phone_exists", "MESSAGE": "This phone number already has an account. Please login instead." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for incomplete account with this phone (verified but no profile, OR unverified)
|
||||||
|
// These accounts can be reused for signup
|
||||||
|
qIncomplete = queryExecute("
|
||||||
|
SELECT UserID, UserUUID
|
||||||
|
FROM Users
|
||||||
|
WHERE UserContactNumber = :phone
|
||||||
|
AND (UserIsContactVerified = 0 OR UserFirstName IS NULL OR LENGTH(TRIM(UserFirstName)) = 0)
|
||||||
|
LIMIT 1
|
||||||
|
", { phone: { value: phone, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
otp = generateOTP();
|
||||||
|
userUUID = "";
|
||||||
|
|
||||||
|
if (qIncomplete.recordCount > 0) {
|
||||||
|
// Update existing incomplete record with new OTP and reset for re-registration
|
||||||
|
userUUID = qIncomplete.UserUUID;
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Users
|
||||||
|
SET UserMobileVerifyCode = :otp,
|
||||||
|
UserIsContactVerified = 0,
|
||||||
|
UserIsActive = 0
|
||||||
|
WHERE UserID = :userId
|
||||||
|
", {
|
||||||
|
otp: { value: otp, cfsqltype: "cf_sql_varchar" },
|
||||||
|
userId: { value: qIncomplete.UserID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
} else {
|
||||||
|
// Create new user record
|
||||||
|
userUUID = replace(createUUID(), "-", "", "all");
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Users (
|
||||||
|
UserContactNumber,
|
||||||
|
UserUUID,
|
||||||
|
UserMobileVerifyCode,
|
||||||
|
UserIsContactVerified,
|
||||||
|
UserIsEmailVerified,
|
||||||
|
UserIsActive,
|
||||||
|
UserAddedOn,
|
||||||
|
UserPassword,
|
||||||
|
UserPromoCode
|
||||||
|
) VALUES (
|
||||||
|
:phone,
|
||||||
|
:uuid,
|
||||||
|
:otp,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
:addedOn,
|
||||||
|
'',
|
||||||
|
:promoCode
|
||||||
|
)
|
||||||
|
", {
|
||||||
|
phone: { value: phone, cfsqltype: "cf_sql_varchar" },
|
||||||
|
uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" },
|
||||||
|
otp: { value: otp, cfsqltype: "cf_sql_varchar" },
|
||||||
|
addedOn: { value: now(), cfsqltype: "cf_sql_timestamp" },
|
||||||
|
promoCode: { value: randRange(1000000, 9999999), cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send OTP via Twilio
|
||||||
|
smsResult = application.twilioObj.sendSMS(
|
||||||
|
recipientNumber: "+1" & phone,
|
||||||
|
messageBody: "Your Payfrit verification code is: " & otp
|
||||||
|
);
|
||||||
|
|
||||||
|
smsStatus = smsResult.success ? "sent" : "failed: " & smsResult.message;
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"UUID": userUUID,
|
||||||
|
"MESSAGE": smsResult.success ? "Verification code sent" : "SMS failed but code created - contact support",
|
||||||
|
"SMS_STATUS": smsStatus
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
72
api/auth/validateToken.cfm
Normal file
72
api/auth/validateToken.cfm
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Validate a user token (for WebSocket server authentication)
|
||||||
|
// Input: Token
|
||||||
|
// Output: { OK: true, UserID: ..., UserType: 'customer'/'worker' }
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
token = trim(structKeyExists(data, "Token") ? data.Token : "");
|
||||||
|
|
||||||
|
if (!len(token)) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "Token is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the token
|
||||||
|
qToken = queryExecute("
|
||||||
|
SELECT ut.UserID, u.UserFirstName, u.UserLastName
|
||||||
|
FROM UserTokens ut
|
||||||
|
JOIN Users u ON u.UserID = ut.UserID
|
||||||
|
WHERE ut.Token = :token
|
||||||
|
LIMIT 1
|
||||||
|
", { token: { value: token, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qToken.recordCount == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "invalid_token", "MESSAGE": "Token is invalid or expired" });
|
||||||
|
}
|
||||||
|
|
||||||
|
userID = qToken.UserID;
|
||||||
|
|
||||||
|
// Determine if user is a worker (has any active employment)
|
||||||
|
qWorker = queryExecute("
|
||||||
|
SELECT COUNT(*) as cnt
|
||||||
|
FROM lt_Users_Businesses_Employees
|
||||||
|
WHERE UserID = :userID AND EmployeeIsActive = 1
|
||||||
|
", { userID: { value: userID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
userType = qWorker.cnt > 0 ? "worker" : "customer";
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"UserID": userID,
|
||||||
|
"UserType": userType,
|
||||||
|
"UserName": trim(qToken.UserFirstName & " " & qToken.UserLastName)
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
96
api/auth/verifyLoginOTP.cfm
Normal file
96
api/auth/verifyLoginOTP.cfm
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Verify OTP for LOGIN (existing verified accounts)
|
||||||
|
*
|
||||||
|
* POST: { "uuid": "...", "otp": "123456" }
|
||||||
|
*
|
||||||
|
* Returns: { OK: true, UserID: 123, Token: "...", UserFirstName: "..." }
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
userUUID = structKeyExists(data, "uuid") ? trim(data.uuid) : "";
|
||||||
|
otp = structKeyExists(data, "otp") ? trim(data.otp) : "";
|
||||||
|
|
||||||
|
if (!len(userUUID) || !len(otp)) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find verified user with matching UUID and OTP
|
||||||
|
qUser = queryExecute("
|
||||||
|
SELECT UserID, UserFirstName, UserLastName
|
||||||
|
FROM Users
|
||||||
|
WHERE UserUUID = :uuid
|
||||||
|
AND UserMobileVerifyCode = :otp
|
||||||
|
AND UserIsContactVerified = 1
|
||||||
|
LIMIT 1
|
||||||
|
", {
|
||||||
|
uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" },
|
||||||
|
otp: { value: otp, cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qUser.recordCount == 0) {
|
||||||
|
// Check if UUID exists but OTP is wrong
|
||||||
|
qCheck = queryExecute("
|
||||||
|
SELECT UserID FROM Users WHERE UserUUID = :uuid AND UserIsContactVerified = 1
|
||||||
|
", { uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qCheck.recordCount > 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "invalid_otp", "MESSAGE": "Invalid code. Please try again." });
|
||||||
|
} else {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "expired", "MESSAGE": "Session expired. Please request a new code." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the OTP (one-time use)
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Users
|
||||||
|
SET UserMobileVerifyCode = ''
|
||||||
|
WHERE UserID = :userId
|
||||||
|
", { userId: { value: qUser.UserID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Create auth token
|
||||||
|
token = replace(createUUID(), "-", "", "all");
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO UserTokens (UserID, Token) VALUES (:userId, :token)
|
||||||
|
", {
|
||||||
|
userId: { value: qUser.UserID, cfsqltype: "cf_sql_integer" },
|
||||||
|
token: { value: token, cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"UserID": qUser.UserID,
|
||||||
|
"Token": token,
|
||||||
|
"UserFirstName": qUser.UserFirstName ?: ""
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
106
api/auth/verifyOTP.cfm
Normal file
106
api/auth/verifyOTP.cfm
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Verify OTP and activate user account
|
||||||
|
*
|
||||||
|
* POST: { "uuid": "...", "otp": "123456" }
|
||||||
|
*
|
||||||
|
* Returns: { OK: true, UserID: 123, Token: "...", NeedsProfile: true/false }
|
||||||
|
*
|
||||||
|
* On success, marks phone as verified and returns auth token.
|
||||||
|
* NeedsProfile indicates if user still needs to provide name/email.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
userUUID = structKeyExists(data, "uuid") ? trim(data.uuid) : "";
|
||||||
|
otp = structKeyExists(data, "otp") ? trim(data.otp) : "";
|
||||||
|
|
||||||
|
if (!len(userUUID) || !len(otp)) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find unverified user with matching UUID and OTP
|
||||||
|
qUser = queryExecute("
|
||||||
|
SELECT UserID, UserFirstName, UserLastName, UserEmailAddress, UserIsEmailVerified
|
||||||
|
FROM Users
|
||||||
|
WHERE UserUUID = :uuid
|
||||||
|
AND UserMobileVerifyCode = :otp
|
||||||
|
AND UserIsContactVerified = 0
|
||||||
|
LIMIT 1
|
||||||
|
", {
|
||||||
|
uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" },
|
||||||
|
otp: { value: otp, cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qUser.recordCount == 0) {
|
||||||
|
// Check if UUID exists but OTP is wrong
|
||||||
|
qCheck = queryExecute("
|
||||||
|
SELECT UserID FROM Users WHERE UserUUID = :uuid AND UserIsContactVerified = 0
|
||||||
|
", { uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qCheck.recordCount > 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "invalid_otp", "MESSAGE": "Invalid verification code. Please try again." });
|
||||||
|
} else {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "expired", "MESSAGE": "Verification expired. Please request a new code." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the OTP code (one-time use) but DON'T mark as verified yet
|
||||||
|
// Account will be marked verified after profile completion
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Users
|
||||||
|
SET UserMobileVerifyCode = ''
|
||||||
|
WHERE UserID = :userId
|
||||||
|
", { userId: { value: qUser.UserID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Create auth token (needed for completeProfile call)
|
||||||
|
token = replace(createUUID(), "-", "", "all");
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO UserTokens (UserID, Token) VALUES (:userId, :token)
|
||||||
|
", {
|
||||||
|
userId: { value: qUser.UserID, cfsqltype: "cf_sql_integer" },
|
||||||
|
token: { value: token, cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Check if profile is complete (has first name)
|
||||||
|
// For new signups, this will always be true
|
||||||
|
needsProfile = !len(trim(qUser.UserFirstName));
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"UserID": qUser.UserID,
|
||||||
|
"Token": token,
|
||||||
|
"NeedsProfile": needsProfile,
|
||||||
|
"UserFirstName": qUser.UserFirstName ?: "",
|
||||||
|
"IsEmailVerified": qUser.UserIsEmailVerified == 1
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
57
api/chat/closeChat.cfm
Normal file
57
api/chat/closeChat.cfm
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Close/complete a chat task
|
||||||
|
// Input: TaskID
|
||||||
|
// Output: { OK: true }
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
taskID = val(structKeyExists(data, "TaskID") ? data.TaskID : 0);
|
||||||
|
|
||||||
|
if (taskID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "TaskID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the task as completed
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Tasks
|
||||||
|
SET TaskCompletedOn = NOW()
|
||||||
|
WHERE TaskID = :taskID
|
||||||
|
AND TaskTypeID = 2
|
||||||
|
AND TaskCompletedOn IS NULL
|
||||||
|
", {
|
||||||
|
taskID: { value: taskID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"MESSAGE": "Chat closed"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
80
api/chat/getActiveChat.cfm
Normal file
80
api/chat/getActiveChat.cfm
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Check for an active (uncompleted) chat task at a business
|
||||||
|
// Input: BusinessID, ServicePointID (optional), UserID (optional)
|
||||||
|
// Output: { OK: true, HAS_ACTIVE_CHAT: true/false, TASK_ID: ... }
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
businessID = val(structKeyExists(data, "BusinessID") ? data.BusinessID : 0);
|
||||||
|
servicePointID = val(structKeyExists(data, "ServicePointID") ? data.ServicePointID : 0);
|
||||||
|
userID = val(structKeyExists(data, "UserID") ? data.UserID : 0);
|
||||||
|
|
||||||
|
if (businessID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for any active chat task at this business (TaskTypeID = 2, not completed)
|
||||||
|
// Priority order:
|
||||||
|
// 1. Chats that are claimed (worker is responding)
|
||||||
|
// 2. Chats that have messages (ongoing conversation)
|
||||||
|
// 3. Most recently created chat
|
||||||
|
qChat = queryExecute("
|
||||||
|
SELECT t.TaskID, t.TaskTitle, t.TaskSourceID,
|
||||||
|
(SELECT COUNT(*) FROM ChatMessages m WHERE m.TaskID = t.TaskID) as MessageCount
|
||||||
|
FROM Tasks t
|
||||||
|
WHERE t.TaskBusinessID = :businessID
|
||||||
|
AND t.TaskTypeID = 2
|
||||||
|
AND t.TaskCompletedOn IS NULL
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN t.TaskClaimedByUserID > 0 THEN 0 ELSE 1 END,
|
||||||
|
(SELECT COUNT(*) FROM ChatMessages m WHERE m.TaskID = t.TaskID) DESC,
|
||||||
|
t.TaskAddedOn DESC
|
||||||
|
LIMIT 1
|
||||||
|
", {
|
||||||
|
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qChat.recordCount > 0) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"HAS_ACTIVE_CHAT": true,
|
||||||
|
"TASK_ID": qChat.TaskID,
|
||||||
|
"TASK_TITLE": qChat.TaskTitle
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"HAS_ACTIVE_CHAT": false,
|
||||||
|
"TASK_ID": 0,
|
||||||
|
"TASK_TITLE": ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
113
api/chat/getMessages.cfm
Normal file
113
api/chat/getMessages.cfm
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Get chat messages for a task
|
||||||
|
// Input: TaskID, AfterMessageID (optional - for pagination/polling)
|
||||||
|
// Output: { OK: true, MESSAGES: [...] }
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
taskID = val(structKeyExists(data, "TaskID") ? data.TaskID : 0);
|
||||||
|
afterMessageID = val(structKeyExists(data, "AfterMessageID") ? data.AfterMessageID : 0);
|
||||||
|
|
||||||
|
if (taskID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "TaskID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get messages
|
||||||
|
if (afterMessageID > 0) {
|
||||||
|
qMessages = queryExecute("
|
||||||
|
SELECT
|
||||||
|
m.MessageID,
|
||||||
|
m.TaskID,
|
||||||
|
m.SenderUserID,
|
||||||
|
m.SenderType,
|
||||||
|
m.MessageText,
|
||||||
|
m.IsRead,
|
||||||
|
m.CreatedOn,
|
||||||
|
u.UserFirstName as SenderName
|
||||||
|
FROM ChatMessages m
|
||||||
|
LEFT JOIN Users u ON u.UserID = m.SenderUserID
|
||||||
|
WHERE m.TaskID = :taskID AND m.MessageID > :afterID
|
||||||
|
ORDER BY m.CreatedOn ASC
|
||||||
|
", {
|
||||||
|
taskID: { value: taskID, cfsqltype: "cf_sql_integer" },
|
||||||
|
afterID: { value: afterMessageID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
} else {
|
||||||
|
qMessages = queryExecute("
|
||||||
|
SELECT
|
||||||
|
m.MessageID,
|
||||||
|
m.TaskID,
|
||||||
|
m.SenderUserID,
|
||||||
|
m.SenderType,
|
||||||
|
m.MessageText,
|
||||||
|
m.IsRead,
|
||||||
|
m.CreatedOn,
|
||||||
|
u.UserFirstName as SenderName
|
||||||
|
FROM ChatMessages m
|
||||||
|
LEFT JOIN Users u ON u.UserID = m.SenderUserID
|
||||||
|
WHERE m.TaskID = :taskID
|
||||||
|
ORDER BY m.CreatedOn ASC
|
||||||
|
", {
|
||||||
|
taskID: { value: taskID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = [];
|
||||||
|
for (msg in qMessages) {
|
||||||
|
arrayAppend(messages, {
|
||||||
|
"MessageID": msg.MessageID,
|
||||||
|
"TaskID": msg.TaskID,
|
||||||
|
"SenderUserID": msg.SenderUserID,
|
||||||
|
"SenderType": msg.SenderType,
|
||||||
|
"SenderName": len(trim(msg.SenderName)) ? msg.SenderName : (msg.SenderType == "customer" ? "Customer" : "Staff"),
|
||||||
|
"Text": msg.MessageText,
|
||||||
|
"IsRead": msg.IsRead == 1,
|
||||||
|
"CreatedOn": dateFormat(msg.CreatedOn, "yyyy-mm-dd") & "T" & timeFormat(msg.CreatedOn, "HH:mm:ss")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if chat/task is closed (completed)
|
||||||
|
qTask = queryExecute("
|
||||||
|
SELECT TaskCompletedOn FROM Tasks WHERE TaskID = :taskID
|
||||||
|
", { taskID: { value: taskID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
chatClosed = false;
|
||||||
|
if (qTask.recordCount > 0 && len(trim(qTask.TaskCompletedOn)) > 0) {
|
||||||
|
chatClosed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"MESSAGES": messages,
|
||||||
|
"COUNT": arrayLen(messages),
|
||||||
|
"CHAT_CLOSED": chatClosed
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
65
api/chat/markRead.cfm
Normal file
65
api/chat/markRead.cfm
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Mark messages as read
|
||||||
|
// Input: TaskID, ReaderType (customer/worker) - marks messages from the OTHER party as read
|
||||||
|
// Output: { OK: true }
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
taskID = val(structKeyExists(data, "TaskID") ? data.TaskID : 0);
|
||||||
|
readerType = lcase(trim(structKeyExists(data, "ReaderType") ? data.ReaderType : ""));
|
||||||
|
|
||||||
|
if (taskID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "TaskID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readerType != "customer" && readerType != "worker") {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "ReaderType must be 'customer' or 'worker'" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark messages from the OTHER party as read
|
||||||
|
// If reader is customer, mark worker messages as read
|
||||||
|
// If reader is worker, mark customer messages as read
|
||||||
|
otherType = readerType == "customer" ? "worker" : "customer";
|
||||||
|
|
||||||
|
queryExecute("
|
||||||
|
UPDATE ChatMessages
|
||||||
|
SET IsRead = 1
|
||||||
|
WHERE TaskID = :taskID AND SenderType = :otherType AND IsRead = 0
|
||||||
|
", {
|
||||||
|
taskID: { value: taskID, cfsqltype: "cf_sql_integer" },
|
||||||
|
otherType: { value: otherType, cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"MESSAGE": "Messages marked as read"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
92
api/chat/sendMessage.cfm
Normal file
92
api/chat/sendMessage.cfm
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Send a chat message (HTTP fallback when WebSocket not available)
|
||||||
|
// Input: TaskID, Message, SenderType (customer/worker)
|
||||||
|
// Output: { OK: true, MessageID: ... }
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
taskID = val(structKeyExists(data, "TaskID") ? data.TaskID : 0);
|
||||||
|
message = trim(structKeyExists(data, "Message") ? data.Message : "");
|
||||||
|
senderType = lcase(trim(structKeyExists(data, "SenderType") ? data.SenderType : "customer"));
|
||||||
|
userID = val(structKeyExists(data, "UserID") ? data.UserID : 0);
|
||||||
|
|
||||||
|
// Also check request scope for authenticated user
|
||||||
|
if (userID == 0 && structKeyExists(request, "UserID")) {
|
||||||
|
userID = request.UserID;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "TaskID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!len(message)) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "Message is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "UserID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate sender type
|
||||||
|
if (senderType != "customer" && senderType != "worker") {
|
||||||
|
senderType = "customer";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify task exists
|
||||||
|
taskQuery = queryExecute("
|
||||||
|
SELECT TaskID, TaskClaimedByUserID FROM Tasks WHERE TaskID = :taskID
|
||||||
|
", { taskID: { value: taskID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (taskQuery.recordCount == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Task not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert message
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO ChatMessages (TaskID, SenderUserID, SenderType, MessageText)
|
||||||
|
VALUES (:taskID, :userID, :senderType, :message)
|
||||||
|
", {
|
||||||
|
taskID: { value: taskID, cfsqltype: "cf_sql_integer" },
|
||||||
|
userID: { value: userID, cfsqltype: "cf_sql_integer" },
|
||||||
|
senderType: { value: senderType, cfsqltype: "cf_sql_varchar" },
|
||||||
|
message: { value: message, cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Get the new message ID
|
||||||
|
result = queryExecute("SELECT LAST_INSERT_ID() as newID", [], { datasource: "payfrit" });
|
||||||
|
messageID = result.newID;
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"MessageID": messageID,
|
||||||
|
"MESSAGE": "Message sent"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
26
api/debug/tables.cfm
Normal file
26
api/debug/tables.cfm
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
try {
|
||||||
|
qTables = queryExecute("SHOW TABLES LIKE '%tate%'", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
tables = [];
|
||||||
|
for (row in qTables) {
|
||||||
|
for (col in row) {
|
||||||
|
arrayAppend(tables, row[col]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"TABLES": tables
|
||||||
|
}));
|
||||||
|
} catch (any e) {
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": e.message
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<cfsetting showdebugoutput="false">
|
<cfsetting showdebugoutput="false">
|
||||||
<cfsetting enablecfoutputonly="true">
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<!--- Force recompile: 2026-01-09 --->
|
||||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
<cfheader name="Cache-Control" value="no-store">
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
|
@ -49,18 +50,22 @@ try {
|
||||||
o.OrderUserID,
|
o.OrderUserID,
|
||||||
o.OrderServicePointID,
|
o.OrderServicePointID,
|
||||||
o.OrderStatusID,
|
o.OrderStatusID,
|
||||||
|
o.OrderTypeID,
|
||||||
o.OrderRemarks,
|
o.OrderRemarks,
|
||||||
o.OrderAddedOn,
|
o.OrderAddedOn,
|
||||||
o.OrderLastEditedOn,
|
o.OrderLastEditedOn,
|
||||||
|
o.OrderSubmittedOn,
|
||||||
u.UserFirstName,
|
u.UserFirstName,
|
||||||
u.UserLastName,
|
u.UserLastName,
|
||||||
u.UserContactNumber,
|
u.UserContactNumber,
|
||||||
u.UserEmailAddress,
|
u.UserEmailAddress,
|
||||||
sp.ServicePointName,
|
sp.ServicePointName,
|
||||||
sp.ServicePointTypeID
|
sp.ServicePointTypeID,
|
||||||
|
b.BusinessName
|
||||||
FROM Orders o
|
FROM Orders o
|
||||||
LEFT JOIN Users u ON u.UserID = o.OrderUserID
|
LEFT JOIN Users u ON u.UserID = o.OrderUserID
|
||||||
LEFT JOIN ServicePoints sp ON sp.ServicePointID = o.OrderServicePointID
|
LEFT JOIN ServicePoints sp ON sp.ServicePointID = o.OrderServicePointID
|
||||||
|
LEFT JOIN Businesses b ON b.BusinessID = o.OrderBusinessID
|
||||||
WHERE o.OrderID = :orderID
|
WHERE o.OrderID = :orderID
|
||||||
", { orderID: orderID });
|
", { orderID: orderID });
|
||||||
|
|
||||||
|
|
@ -71,7 +76,7 @@ try {
|
||||||
abort;
|
abort;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get line items
|
// Get line items (excluding deleted items)
|
||||||
qItems = queryExecute("
|
qItems = queryExecute("
|
||||||
SELECT
|
SELECT
|
||||||
oli.OrderLineItemID,
|
oli.OrderLineItemID,
|
||||||
|
|
@ -81,10 +86,12 @@ try {
|
||||||
oli.OrderLineItemPrice,
|
oli.OrderLineItemPrice,
|
||||||
oli.OrderLineItemRemark,
|
oli.OrderLineItemRemark,
|
||||||
i.ItemName,
|
i.ItemName,
|
||||||
i.ItemPrice
|
i.ItemPrice,
|
||||||
|
i.ItemIsCheckedByDefault
|
||||||
FROM OrderLineItems oli
|
FROM OrderLineItems oli
|
||||||
INNER JOIN Items i ON i.ItemID = oli.OrderLineItemItemID
|
INNER JOIN Items i ON i.ItemID = oli.OrderLineItemItemID
|
||||||
WHERE oli.OrderLineItemOrderID = :orderID
|
WHERE oli.OrderLineItemOrderID = :orderID
|
||||||
|
AND oli.OrderLineItemIsDeleted = 0
|
||||||
ORDER BY oli.OrderLineItemID
|
ORDER BY oli.OrderLineItemID
|
||||||
", { orderID: orderID });
|
", { orderID: orderID });
|
||||||
|
|
||||||
|
|
@ -102,6 +109,7 @@ try {
|
||||||
"Quantity": row.OrderLineItemQuantity,
|
"Quantity": row.OrderLineItemQuantity,
|
||||||
"UnitPrice": row.OrderLineItemPrice,
|
"UnitPrice": row.OrderLineItemPrice,
|
||||||
"Remarks": row.OrderLineItemRemark,
|
"Remarks": row.OrderLineItemRemark,
|
||||||
|
"IsDefault": (row.ItemIsCheckedByDefault == 1),
|
||||||
"Modifiers": []
|
"Modifiers": []
|
||||||
};
|
};
|
||||||
itemsById[row.OrderLineItemID] = item;
|
itemsById[row.OrderLineItemID] = item;
|
||||||
|
|
@ -159,14 +167,18 @@ try {
|
||||||
order = {
|
order = {
|
||||||
"OrderID": qOrder.OrderID,
|
"OrderID": qOrder.OrderID,
|
||||||
"BusinessID": qOrder.OrderBusinessID,
|
"BusinessID": qOrder.OrderBusinessID,
|
||||||
|
"BusinessName": qOrder.BusinessName ?: "",
|
||||||
"Status": qOrder.OrderStatusID,
|
"Status": qOrder.OrderStatusID,
|
||||||
"StatusText": getStatusText(qOrder.OrderStatusID),
|
"StatusText": getStatusText(qOrder.OrderStatusID),
|
||||||
|
"OrderTypeID": qOrder.OrderTypeID ?: 0,
|
||||||
|
"OrderTypeName": getOrderTypeName(qOrder.OrderTypeID ?: 0),
|
||||||
"Subtotal": subtotal,
|
"Subtotal": subtotal,
|
||||||
"Tax": tax,
|
"Tax": tax,
|
||||||
"Tip": tip,
|
"Tip": tip,
|
||||||
"Total": total,
|
"Total": total,
|
||||||
"Notes": qOrder.OrderRemarks,
|
"Notes": qOrder.OrderRemarks,
|
||||||
"CreatedOn": dateTimeFormat(qOrder.OrderAddedOn, "yyyy-mm-dd HH:nn:ss"),
|
"CreatedOn": dateTimeFormat(qOrder.OrderAddedOn, "yyyy-mm-dd HH:nn:ss"),
|
||||||
|
"SubmittedOn": len(qOrder.OrderSubmittedOn) ? dateTimeFormat(qOrder.OrderSubmittedOn, "yyyy-mm-dd HH:nn:ss") : "",
|
||||||
"UpdatedOn": len(qOrder.OrderLastEditedOn) ? dateTimeFormat(qOrder.OrderLastEditedOn, "yyyy-mm-dd HH:nn:ss") : "",
|
"UpdatedOn": len(qOrder.OrderLastEditedOn) ? dateTimeFormat(qOrder.OrderLastEditedOn, "yyyy-mm-dd HH:nn:ss") : "",
|
||||||
"Customer": {
|
"Customer": {
|
||||||
"UserID": qOrder.OrderUserID,
|
"UserID": qOrder.OrderUserID,
|
||||||
|
|
@ -193,7 +205,7 @@ try {
|
||||||
|
|
||||||
writeOutput(serializeJSON(response));
|
writeOutput(serializeJSON(response));
|
||||||
|
|
||||||
// Helper function
|
// Helper functions
|
||||||
function getStatusText(status) {
|
function getStatusText(status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 0: return "Cart";
|
case 0: return "Cart";
|
||||||
|
|
@ -205,4 +217,13 @@ function getStatusText(status) {
|
||||||
default: return "Unknown";
|
default: return "Unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOrderTypeName(orderType) {
|
||||||
|
switch (orderType) {
|
||||||
|
case 1: return "Dine-in";
|
||||||
|
case 2: return "Takeaway";
|
||||||
|
case 3: return "Delivery";
|
||||||
|
default: return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
</cfscript>
|
</cfscript>
|
||||||
|
|
|
||||||
97
api/portal/team.cfm
Normal file
97
api/portal/team.cfm
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/*
|
||||||
|
PATH: /api/portal/team.cfm
|
||||||
|
|
||||||
|
INPUT (JSON):
|
||||||
|
{ "BusinessID": 17 }
|
||||||
|
|
||||||
|
OUTPUT (JSON):
|
||||||
|
{ OK: true, TEAM: [ { EmployeeID, UserID, Name, Email, Phone, StatusID, StatusName, IsActive } ] }
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (businessId <= 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_business_id" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get employees for this business with user details
|
||||||
|
qTeam = queryExecute("
|
||||||
|
SELECT
|
||||||
|
e.EmployeeID,
|
||||||
|
e.UserID,
|
||||||
|
e.EmployeeStatusID,
|
||||||
|
e.EmployeeIsActive,
|
||||||
|
u.UserFirstName,
|
||||||
|
u.UserLastName,
|
||||||
|
u.UserEmailAddress,
|
||||||
|
u.UserContactNumber,
|
||||||
|
CASE e.EmployeeStatusID
|
||||||
|
WHEN 0 THEN 'Pending'
|
||||||
|
WHEN 1 THEN 'Invited'
|
||||||
|
WHEN 2 THEN 'Active'
|
||||||
|
WHEN 3 THEN 'Suspended'
|
||||||
|
ELSE 'Unknown'
|
||||||
|
END AS StatusName
|
||||||
|
FROM lt_Users_Businesses_Employees e
|
||||||
|
JOIN Users u ON e.UserID = u.UserID
|
||||||
|
WHERE e.BusinessID = ?
|
||||||
|
ORDER BY e.EmployeeIsActive DESC, u.UserFirstName ASC
|
||||||
|
", [
|
||||||
|
{ value: businessId, cfsqltype: "cf_sql_integer" }
|
||||||
|
], { datasource: "payfrit" });
|
||||||
|
|
||||||
|
team = [];
|
||||||
|
for (row in qTeam) {
|
||||||
|
arrayAppend(team, {
|
||||||
|
"EmployeeID": row.EmployeeID,
|
||||||
|
"UserID": row.UserID,
|
||||||
|
"Name": trim(row.UserFirstName & " " & row.UserLastName),
|
||||||
|
"FirstName": row.UserFirstName,
|
||||||
|
"LastName": row.UserLastName,
|
||||||
|
"Email": row.UserEmailAddress,
|
||||||
|
"Phone": row.UserContactNumber,
|
||||||
|
"StatusID": row.EmployeeStatusID,
|
||||||
|
"StatusName": row.StatusName,
|
||||||
|
"IsActive": row.EmployeeIsActive == 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"TEAM": team,
|
||||||
|
"COUNT": arrayLen(team)
|
||||||
|
}));
|
||||||
|
abort;
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
139
api/tasks/callServer.cfm
Normal file
139
api/tasks/callServer.cfm
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Customer calls server to their table
|
||||||
|
// Input: BusinessID, ServicePointID, OrderID (optional), Message (optional)
|
||||||
|
// Output: { OK: true, TASK_ID: ... }
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
businessID = val(structKeyExists(data, "BusinessID") ? data.BusinessID : 0);
|
||||||
|
servicePointID = val(structKeyExists(data, "ServicePointID") ? data.ServicePointID : 0);
|
||||||
|
orderID = val(structKeyExists(data, "OrderID") ? data.OrderID : 0);
|
||||||
|
message = trim(structKeyExists(data, "Message") ? data.Message : "");
|
||||||
|
userID = val(structKeyExists(data, "UserID") ? data.UserID : 0);
|
||||||
|
|
||||||
|
if (businessID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (servicePointID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "ServicePointID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get service point info (table name)
|
||||||
|
spQuery = queryExecute("
|
||||||
|
SELECT ServicePointName FROM ServicePoints WHERE ServicePointID = :spID
|
||||||
|
", { spID: { value: servicePointID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
tableName = spQuery.recordCount ? spQuery.ServicePointName : "Table ##" & servicePointID;
|
||||||
|
|
||||||
|
// Get user name if available
|
||||||
|
userName = "";
|
||||||
|
if (userID > 0) {
|
||||||
|
userQuery = queryExecute("
|
||||||
|
SELECT UserFirstName FROM Users WHERE UserID = :userID
|
||||||
|
", { userID: { value: userID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
if (userQuery.recordCount && len(trim(userQuery.UserFirstName))) {
|
||||||
|
userName = userQuery.UserFirstName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create task title and details
|
||||||
|
taskTitle = "Service Request - " & tableName;
|
||||||
|
|
||||||
|
taskDetails = "";
|
||||||
|
if (len(userName)) {
|
||||||
|
taskDetails &= "Customer: " & userName & chr(10);
|
||||||
|
}
|
||||||
|
if (len(message)) {
|
||||||
|
taskDetails &= "Request: " & message;
|
||||||
|
} else {
|
||||||
|
taskDetails &= "Customer is requesting assistance";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up or create a "Service" category for this business
|
||||||
|
catQuery = queryExecute("
|
||||||
|
SELECT TaskCategoryID FROM TaskCategories
|
||||||
|
WHERE TaskCategoryBusinessID = :businessID AND TaskCategoryName = 'Service'
|
||||||
|
LIMIT 1
|
||||||
|
", { businessID: { value: businessID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (catQuery.recordCount == 0) {
|
||||||
|
// Create the category
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO TaskCategories (TaskCategoryBusinessID, TaskCategoryName, TaskCategoryColor)
|
||||||
|
VALUES (:businessID, 'Service', '##FF9800')
|
||||||
|
", { businessID: { value: businessID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
catResult = queryExecute("SELECT LAST_INSERT_ID() as newID", [], { datasource: "payfrit" });
|
||||||
|
categoryID = catResult.newID;
|
||||||
|
} else {
|
||||||
|
categoryID = catQuery.TaskCategoryID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert task
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Tasks (
|
||||||
|
TaskBusinessID,
|
||||||
|
TaskCategoryID,
|
||||||
|
TaskOrderID,
|
||||||
|
TaskTypeID,
|
||||||
|
TaskTitle,
|
||||||
|
TaskDetails,
|
||||||
|
TaskClaimedByUserID,
|
||||||
|
TaskAddedOn
|
||||||
|
) VALUES (
|
||||||
|
:businessID,
|
||||||
|
:categoryID,
|
||||||
|
:orderID,
|
||||||
|
1,
|
||||||
|
:title,
|
||||||
|
:details,
|
||||||
|
0,
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
", {
|
||||||
|
businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
|
||||||
|
categoryID: { value: categoryID, cfsqltype: "cf_sql_integer" },
|
||||||
|
orderID: { value: orderID > 0 ? orderID : javaCast("null", ""), cfsqltype: "cf_sql_integer", null: orderID == 0 },
|
||||||
|
title: { value: taskTitle, cfsqltype: "cf_sql_varchar" },
|
||||||
|
details: { value: taskDetails, cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Get the new task ID
|
||||||
|
result = queryExecute("SELECT LAST_INSERT_ID() as newID", [], { datasource: "payfrit" });
|
||||||
|
taskID = result.newID;
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"TASK_ID": taskID,
|
||||||
|
"MESSAGE": "Server has been notified"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
|
|
@ -36,9 +36,9 @@
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<cftry>
|
<cftry>
|
||||||
<!--- Verify task exists and is claimed by this user --->
|
<!--- Verify task exists --->
|
||||||
<cfset qTask = queryExecute("
|
<cfset qTask = queryExecute("
|
||||||
SELECT TaskID, TaskClaimedByUserID, TaskCompletedOn, TaskOrderID
|
SELECT TaskID, TaskClaimedByUserID, TaskCompletedOn, TaskOrderID, TaskTypeID
|
||||||
FROM Tasks
|
FROM Tasks
|
||||||
WHERE TaskID = ?
|
WHERE TaskID = ?
|
||||||
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
@ -47,11 +47,14 @@
|
||||||
<cfset apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Task not found." })>
|
<cfset apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Task not found." })>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<cfif qTask.TaskClaimedByUserID EQ 0>
|
<!--- Chat tasks (TaskTypeID = 2) can be closed without being claimed first --->
|
||||||
|
<cfset isChatTask = (qTask.TaskTypeID EQ 2)>
|
||||||
|
|
||||||
|
<cfif (NOT isChatTask) AND (qTask.TaskClaimedByUserID EQ 0)>
|
||||||
<cfset apiAbort({ "OK": false, "ERROR": "not_claimed", "MESSAGE": "Task has not been claimed yet." })>
|
<cfset apiAbort({ "OK": false, "ERROR": "not_claimed", "MESSAGE": "Task has not been claimed yet." })>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<cfif UserID GT 0 AND qTask.TaskClaimedByUserID NEQ UserID>
|
<cfif (NOT isChatTask) AND (UserID GT 0) AND (qTask.TaskClaimedByUserID NEQ UserID)>
|
||||||
<cfset apiAbort({ "OK": false, "ERROR": "not_yours", "MESSAGE": "This task was claimed by someone else." })>
|
<cfset apiAbort({ "OK": false, "ERROR": "not_yours", "MESSAGE": "This task was claimed by someone else." })>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
|
|
|
||||||
80
api/tasks/completeChat.cfm
Normal file
80
api/tasks/completeChat.cfm
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
|
||||||
|
<cffunction name="apiAbort" access="public" returntype="void" output="true">
|
||||||
|
<cfargument name="payload" type="struct" required="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8">
|
||||||
|
<cfoutput>#serializeJSON(arguments.payload)#</cfoutput>
|
||||||
|
<cfabort>
|
||||||
|
</cffunction>
|
||||||
|
|
||||||
|
<cffunction name="readJsonBody" access="public" returntype="struct" output="false">
|
||||||
|
<cfset var raw = getHttpRequestData().content>
|
||||||
|
<cfif isNull(raw) OR len(trim(raw)) EQ 0>
|
||||||
|
<cfreturn {}>
|
||||||
|
</cfif>
|
||||||
|
<cftry>
|
||||||
|
<cfset var data = deserializeJSON(raw)>
|
||||||
|
<cfif isStruct(data)>
|
||||||
|
<cfreturn data>
|
||||||
|
<cfelse>
|
||||||
|
<cfreturn {}>
|
||||||
|
</cfif>
|
||||||
|
<cfcatch>
|
||||||
|
<cfreturn {}>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
</cffunction>
|
||||||
|
|
||||||
|
<cfset data = readJsonBody()>
|
||||||
|
<cfset TaskID = val( structKeyExists(data,"TaskID") ? data.TaskID : 0 )>
|
||||||
|
|
||||||
|
<cfif TaskID LTE 0>
|
||||||
|
<cfset apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "TaskID is required." })>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cftry>
|
||||||
|
<!--- Verify task exists and is a chat task --->
|
||||||
|
<cfset qTask = queryExecute("
|
||||||
|
SELECT TaskID, TaskClaimedByUserID, TaskCompletedOn, TaskOrderID, TaskTypeID
|
||||||
|
FROM Tasks
|
||||||
|
WHERE TaskID = ?
|
||||||
|
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<cfif qTask.recordCount EQ 0>
|
||||||
|
<cfset apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Task not found." })>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Only allow chat tasks (TaskTypeID = 2) --->
|
||||||
|
<cfif qTask.TaskTypeID NEQ 2>
|
||||||
|
<cfset apiAbort({ "OK": false, "ERROR": "not_chat", "MESSAGE": "This endpoint is only for chat tasks." })>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Check if already completed --->
|
||||||
|
<cfif len(trim(qTask.TaskCompletedOn)) GT 0>
|
||||||
|
<cfset apiAbort({ "OK": false, "ERROR": "already_completed", "MESSAGE": "Chat has already been closed." })>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Mark task as completed --->
|
||||||
|
<cfset queryExecute("
|
||||||
|
UPDATE Tasks
|
||||||
|
SET TaskCompletedOn = NOW()
|
||||||
|
WHERE TaskID = ?
|
||||||
|
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<cfset apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"ERROR": "",
|
||||||
|
"MESSAGE": "Chat closed successfully.",
|
||||||
|
"TaskID": TaskID
|
||||||
|
})>
|
||||||
|
|
||||||
|
<cfcatch>
|
||||||
|
<cfset apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": "Error closing chat",
|
||||||
|
"DETAIL": cfcatch.message
|
||||||
|
})>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
154
api/tasks/createChat.cfm
Normal file
154
api/tasks/createChat.cfm
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Customer initiates a chat with staff
|
||||||
|
// Input: BusinessID, ServicePointID, OrderID (optional), Message (optional initial message)
|
||||||
|
// Output: { OK: true, TaskID: ... }
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
businessID = val(structKeyExists(data, "BusinessID") ? data.BusinessID : 0);
|
||||||
|
servicePointID = val(structKeyExists(data, "ServicePointID") ? data.ServicePointID : 0);
|
||||||
|
orderID = val(structKeyExists(data, "OrderID") ? data.OrderID : 0);
|
||||||
|
initialMessage = trim(structKeyExists(data, "Message") ? data.Message : "");
|
||||||
|
userID = val(structKeyExists(data, "UserID") ? data.UserID : 0);
|
||||||
|
|
||||||
|
if (businessID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (servicePointID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "ServicePointID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get service point info (table name)
|
||||||
|
spQuery = queryExecute("
|
||||||
|
SELECT ServicePointName FROM ServicePoints WHERE ServicePointID = :spID
|
||||||
|
", { spID: { value: servicePointID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
tableName = spQuery.recordCount ? spQuery.ServicePointName : "Table ##" & servicePointID;
|
||||||
|
|
||||||
|
// Get user name if available
|
||||||
|
userName = "";
|
||||||
|
if (userID > 0) {
|
||||||
|
userQuery = queryExecute("
|
||||||
|
SELECT UserFirstName FROM Users WHERE UserID = :userID
|
||||||
|
", { userID: { value: userID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
if (userQuery.recordCount && len(trim(userQuery.UserFirstName))) {
|
||||||
|
userName = userQuery.UserFirstName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create task title
|
||||||
|
taskTitle = "Chat - " & tableName;
|
||||||
|
if (len(userName)) {
|
||||||
|
taskTitle = "Chat - " & userName & " (" & tableName & ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
taskDetails = "Customer initiated chat";
|
||||||
|
if (len(initialMessage)) {
|
||||||
|
taskDetails = initialMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up or create a "Chat" category for this business
|
||||||
|
catQuery = queryExecute("
|
||||||
|
SELECT TaskCategoryID FROM TaskCategories
|
||||||
|
WHERE TaskCategoryBusinessID = :businessID AND TaskCategoryName = 'Chat'
|
||||||
|
LIMIT 1
|
||||||
|
", { businessID: { value: businessID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (catQuery.recordCount == 0) {
|
||||||
|
// Create the category (blue color for chat)
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO TaskCategories (TaskCategoryBusinessID, TaskCategoryName, TaskCategoryColor)
|
||||||
|
VALUES (:businessID, 'Chat', '##2196F3')
|
||||||
|
", { businessID: { value: businessID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
catResult = queryExecute("SELECT LAST_INSERT_ID() as newID", [], { datasource: "payfrit" });
|
||||||
|
categoryID = catResult.newID;
|
||||||
|
} else {
|
||||||
|
categoryID = catQuery.TaskCategoryID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert task with TaskTypeID = 2 (Chat)
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Tasks (
|
||||||
|
TaskBusinessID,
|
||||||
|
TaskCategoryID,
|
||||||
|
TaskOrderID,
|
||||||
|
TaskTypeID,
|
||||||
|
TaskTitle,
|
||||||
|
TaskDetails,
|
||||||
|
TaskClaimedByUserID,
|
||||||
|
TaskSourceType,
|
||||||
|
TaskSourceID,
|
||||||
|
TaskAddedOn
|
||||||
|
) VALUES (
|
||||||
|
:businessID,
|
||||||
|
:categoryID,
|
||||||
|
:orderID,
|
||||||
|
2,
|
||||||
|
:title,
|
||||||
|
:details,
|
||||||
|
0,
|
||||||
|
'servicepoint',
|
||||||
|
:servicePointID,
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
", {
|
||||||
|
businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
|
||||||
|
categoryID: { value: categoryID, cfsqltype: "cf_sql_integer" },
|
||||||
|
orderID: { value: orderID > 0 ? orderID : javaCast("null", ""), cfsqltype: "cf_sql_integer", null: orderID == 0 },
|
||||||
|
title: { value: taskTitle, cfsqltype: "cf_sql_varchar" },
|
||||||
|
details: { value: taskDetails, cfsqltype: "cf_sql_varchar" },
|
||||||
|
servicePointID: { value: servicePointID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Get the new task ID
|
||||||
|
result = queryExecute("SELECT LAST_INSERT_ID() as newID", [], { datasource: "payfrit" });
|
||||||
|
taskID = result.newID;
|
||||||
|
|
||||||
|
// If there's an initial message, save it
|
||||||
|
if (len(initialMessage) && userID > 0) {
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO ChatMessages (TaskID, SenderUserID, SenderType, MessageText)
|
||||||
|
VALUES (:taskID, :userID, 'customer', :message)
|
||||||
|
", {
|
||||||
|
taskID: { value: taskID, cfsqltype: "cf_sql_integer" },
|
||||||
|
userID: { value: userID, cfsqltype: "cf_sql_integer" },
|
||||||
|
message: { value: initialMessage, cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"TaskID": taskID,
|
||||||
|
"MESSAGE": "Chat started"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
|
|
@ -78,6 +78,22 @@
|
||||||
<cfset taskTitle = "Order ##" & qTask.TaskOrderID>
|
<cfset taskTitle = "Order ##" & qTask.TaskOrderID>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Check if user photo file exists (try both .jpg and .png) --->
|
||||||
|
<cfset customerPhotoUrl = "">
|
||||||
|
<cfif qTask.CustomerUserID GT 0>
|
||||||
|
<cfset uploadDir = expandPath("/uploads/users/")>
|
||||||
|
<cfset jpgPath = uploadDir & qTask.CustomerUserID & ".jpg">
|
||||||
|
<cfset pngPath = uploadDir & qTask.CustomerUserID & ".png">
|
||||||
|
<cfset pngPathUpper = uploadDir & qTask.CustomerUserID & ".PNG">
|
||||||
|
<cfif fileExists(jpgPath)>
|
||||||
|
<cfset customerPhotoUrl = "https://biz.payfrit.com/uploads/users/" & qTask.CustomerUserID & ".jpg">
|
||||||
|
<cfelseif fileExists(pngPath)>
|
||||||
|
<cfset customerPhotoUrl = "https://biz.payfrit.com/uploads/users/" & qTask.CustomerUserID & ".png">
|
||||||
|
<cfelseif fileExists(pngPathUpper)>
|
||||||
|
<cfset customerPhotoUrl = "https://biz.payfrit.com/uploads/users/" & qTask.CustomerUserID & ".PNG">
|
||||||
|
</cfif>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
<cfset result = {
|
<cfset result = {
|
||||||
"TaskID": qTask.TaskID,
|
"TaskID": qTask.TaskID,
|
||||||
"TaskBusinessID": qTask.TaskBusinessID,
|
"TaskBusinessID": qTask.TaskBusinessID,
|
||||||
|
|
@ -100,7 +116,7 @@
|
||||||
"CustomerFirstName": qTask.UserFirstName ?: "",
|
"CustomerFirstName": qTask.UserFirstName ?: "",
|
||||||
"CustomerLastName": qTask.UserLastName ?: "",
|
"CustomerLastName": qTask.UserLastName ?: "",
|
||||||
"CustomerPhone": qTask.UserContactNumber ?: "",
|
"CustomerPhone": qTask.UserContactNumber ?: "",
|
||||||
"CustomerPhotoUrl": qTask.CustomerUserID GT 0 ? "https://biz.payfrit.com/uploads/users/" & qTask.CustomerUserID & ".jpg" : "",
|
"CustomerPhotoUrl": customerPhotoUrl,
|
||||||
"BeaconUUID": "",
|
"BeaconUUID": "",
|
||||||
"LineItems": []
|
"LineItems": []
|
||||||
}>
|
}>
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@
|
||||||
"TaskBusinessID": qTasks.TaskBusinessID,
|
"TaskBusinessID": qTasks.TaskBusinessID,
|
||||||
"BusinessName": qTasks.BusinessName,
|
"BusinessName": qTasks.BusinessName,
|
||||||
"TaskCategoryID": qTasks.TaskCategoryID,
|
"TaskCategoryID": qTasks.TaskCategoryID,
|
||||||
|
"TaskTypeID": qTasks.TaskTypeID,
|
||||||
"TaskTitle": taskTitle,
|
"TaskTitle": taskTitle,
|
||||||
"TaskDetails": "",
|
"TaskDetails": "",
|
||||||
"TaskCreatedOn": dateFormat(qTasks.TaskAddedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskAddedOn, "HH:mm:ss"),
|
"TaskCreatedOn": dateFormat(qTasks.TaskAddedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskAddedOn, "HH:mm:ss"),
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@
|
||||||
t.TaskCategoryID,
|
t.TaskCategoryID,
|
||||||
t.TaskOrderID,
|
t.TaskOrderID,
|
||||||
t.TaskTypeID,
|
t.TaskTypeID,
|
||||||
|
t.TaskTitle,
|
||||||
|
t.TaskDetails,
|
||||||
t.TaskAddedOn,
|
t.TaskAddedOn,
|
||||||
t.TaskClaimedByUserID,
|
t.TaskClaimedByUserID,
|
||||||
tc.TaskCategoryName,
|
tc.TaskCategoryName,
|
||||||
|
|
@ -67,18 +69,23 @@
|
||||||
<cfset tasks = []>
|
<cfset tasks = []>
|
||||||
|
|
||||||
<cfloop query="qTasks">
|
<cfloop query="qTasks">
|
||||||
<!--- Build title based on task type --->
|
<!--- Use stored title if available, otherwise build from order --->
|
||||||
<cfset taskTitle = "Task ##" & qTasks.TaskID>
|
<cfset taskTitle = "">
|
||||||
<cfset taskDetails = "">
|
<cfif len(trim(qTasks.TaskTitle))>
|
||||||
|
<cfset taskTitle = qTasks.TaskTitle>
|
||||||
<cfif qTasks.TaskOrderID GT 0>
|
<cfelseif qTasks.TaskOrderID GT 0>
|
||||||
<cfset taskTitle = "Order ##" & qTasks.TaskOrderID>
|
<cfset taskTitle = "Order ##" & qTasks.TaskOrderID>
|
||||||
|
<cfelse>
|
||||||
|
<cfset taskTitle = "Task ##" & qTasks.TaskID>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
|
<cfset taskDetails = len(trim(qTasks.TaskDetails)) ? qTasks.TaskDetails : "">
|
||||||
|
|
||||||
<cfset arrayAppend(tasks, {
|
<cfset arrayAppend(tasks, {
|
||||||
"TaskID": qTasks.TaskID,
|
"TaskID": qTasks.TaskID,
|
||||||
"TaskBusinessID": qTasks.TaskBusinessID,
|
"TaskBusinessID": qTasks.TaskBusinessID,
|
||||||
"TaskCategoryID": qTasks.TaskCategoryID,
|
"TaskCategoryID": qTasks.TaskCategoryID,
|
||||||
|
"TaskTypeID": qTasks.TaskTypeID,
|
||||||
"TaskTitle": taskTitle,
|
"TaskTitle": taskTitle,
|
||||||
"TaskDetails": taskDetails,
|
"TaskDetails": taskDetails,
|
||||||
"TaskCreatedOn": dateFormat(qTasks.TaskAddedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskAddedOn, "HH:mm:ss"),
|
"TaskCreatedOn": dateFormat(qTasks.TaskAddedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskAddedOn, "HH:mm:ss"),
|
||||||
|
|
|
||||||
93
api/users/search.cfm
Normal file
93
api/users/search.cfm
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Search users by phone, email, or username
|
||||||
|
// Input: Query (search term)
|
||||||
|
// Output: { OK: true, USERS: [...] }
|
||||||
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
query = structKeyExists(data, "Query") ? trim(data.Query) : "";
|
||||||
|
currentUserId = val(structKeyExists(data, "CurrentUserID") ? data.CurrentUserID : 0);
|
||||||
|
|
||||||
|
if (len(query) < 3) {
|
||||||
|
apiAbort({ "OK": true, "USERS": [], "MESSAGE": "Query must be at least 3 characters" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search by phone, email, or first/last name
|
||||||
|
// Exclude the current user from results
|
||||||
|
searchTerm = "%" & query & "%";
|
||||||
|
|
||||||
|
qUsers = queryExecute("
|
||||||
|
SELECT
|
||||||
|
u.UserID,
|
||||||
|
u.UserFirstName,
|
||||||
|
u.UserLastName,
|
||||||
|
u.UserEmail,
|
||||||
|
u.UserPhone,
|
||||||
|
u.UserAvatarPath
|
||||||
|
FROM Users u
|
||||||
|
WHERE u.UserID != :currentUserId
|
||||||
|
AND (
|
||||||
|
u.UserPhone LIKE :searchTerm
|
||||||
|
OR u.UserEmail LIKE :searchTerm
|
||||||
|
OR u.UserFirstName LIKE :searchTerm
|
||||||
|
OR u.UserLastName LIKE :searchTerm
|
||||||
|
OR CONCAT(u.UserFirstName, ' ', u.UserLastName) LIKE :searchTerm
|
||||||
|
)
|
||||||
|
ORDER BY u.UserFirstName, u.UserLastName
|
||||||
|
LIMIT 10
|
||||||
|
", {
|
||||||
|
currentUserId: { value: currentUserId, cfsqltype: "cf_sql_integer" },
|
||||||
|
searchTerm: { value: searchTerm, cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
users = [];
|
||||||
|
for (user in qUsers) {
|
||||||
|
// Mask phone number for privacy (show last 4 digits only)
|
||||||
|
maskedPhone = "";
|
||||||
|
if (len(trim(user.UserPhone)) >= 4) {
|
||||||
|
maskedPhone = "***-***-" & right(user.UserPhone, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
arrayAppend(users, {
|
||||||
|
"UserID": user.UserID,
|
||||||
|
"Name": trim(user.UserFirstName & " " & user.UserLastName),
|
||||||
|
"Email": user.UserEmail,
|
||||||
|
"Phone": maskedPhone,
|
||||||
|
"AvatarUrl": len(trim(user.UserAvatarPath)) ? "https://biz.payfrit.com/uploads/" & user.UserAvatarPath : ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"USERS": users,
|
||||||
|
"COUNT": arrayLen(users)
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
32
hud/hud.js
32
hud/hud.js
|
|
@ -12,7 +12,7 @@ const HUD = {
|
||||||
pollInterval: 1000, // Poll every second for smooth animation
|
pollInterval: 1000, // Poll every second for smooth animation
|
||||||
targetSeconds: 60, // Target time to accept a task
|
targetSeconds: 60, // Target time to accept a task
|
||||||
apiBaseUrl: '/api/tasks', // API endpoint
|
apiBaseUrl: '/api/tasks', // API endpoint
|
||||||
businessId: 1, // TODO: Get from login/URL
|
businessId: parseInt(new URLSearchParams(window.location.search).get('b')) || 17,
|
||||||
},
|
},
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
@ -150,10 +150,14 @@ const HUD = {
|
||||||
bar.dataset.taskId = task.TaskID;
|
bar.dataset.taskId = task.TaskID;
|
||||||
bar.dataset.category = task.TaskCategoryID || 1;
|
bar.dataset.category = task.TaskCategoryID || 1;
|
||||||
|
|
||||||
|
// Apply dynamic category color
|
||||||
|
const categoryColor = this.getCategoryColor(task);
|
||||||
|
bar.style.setProperty('--bar-color', categoryColor);
|
||||||
|
|
||||||
bar.innerHTML = `
|
bar.innerHTML = `
|
||||||
<span class="task-time"></span>
|
<span class="task-time"></span>
|
||||||
<span class="task-id">#${task.TaskID}</span>
|
<span class="task-id">#${task.TaskID}</span>
|
||||||
<span class="task-label">${this.getCategoryName(task.TaskCategoryID)}</span>
|
<span class="task-label">${this.getCategoryName(task)}</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Touch/click handlers
|
// Touch/click handlers
|
||||||
|
|
@ -214,9 +218,20 @@ const HUD = {
|
||||||
return `${mins}:${String(secs).padStart(2, '0')}`;
|
return `${mins}:${String(secs).padStart(2, '0')}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get category name
|
// Get category name - use task's category name if available, fall back to hardcoded
|
||||||
getCategoryName(categoryId) {
|
getCategoryName(task) {
|
||||||
return this.categories[categoryId]?.name || 'Task';
|
if (task && task.TaskCategoryName) {
|
||||||
|
return task.TaskCategoryName;
|
||||||
|
}
|
||||||
|
return this.categories[task?.TaskCategoryID]?.name || 'Task';
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get category color - use task's category color if available, fall back to hardcoded
|
||||||
|
getCategoryColor(task) {
|
||||||
|
if (task && task.TaskCategoryColor) {
|
||||||
|
return task.TaskCategoryColor;
|
||||||
|
}
|
||||||
|
return this.categories[task?.TaskCategoryID]?.color || '#888888';
|
||||||
},
|
},
|
||||||
|
|
||||||
// Handle bar press (start long press timer)
|
// Handle bar press (start long press timer)
|
||||||
|
|
@ -250,16 +265,17 @@ const HUD = {
|
||||||
const overlay = document.getElementById('taskOverlay');
|
const overlay = document.getElementById('taskOverlay');
|
||||||
const detail = document.getElementById('taskDetail');
|
const detail = document.getElementById('taskDetail');
|
||||||
|
|
||||||
const category = this.categories[task.TaskCategoryID] || {};
|
|
||||||
const elapsed = this.getElapsedSeconds(task.TaskCreatedOn);
|
const elapsed = this.getElapsedSeconds(task.TaskCreatedOn);
|
||||||
|
const categoryName = this.getCategoryName(task);
|
||||||
|
const categoryColor = this.getCategoryColor(task);
|
||||||
|
|
||||||
document.getElementById('detailTitle').textContent = task.TaskTitle || `Task #${task.TaskID}`;
|
document.getElementById('detailTitle').textContent = task.TaskTitle || `Task #${task.TaskID}`;
|
||||||
document.getElementById('detailCategory').textContent = category.name || 'Unknown';
|
document.getElementById('detailCategory').textContent = categoryName;
|
||||||
document.getElementById('detailCreated').textContent = new Date(task.TaskCreatedOn).toLocaleTimeString();
|
document.getElementById('detailCreated').textContent = new Date(task.TaskCreatedOn).toLocaleTimeString();
|
||||||
document.getElementById('detailWaiting').textContent = this.formatElapsed(elapsed);
|
document.getElementById('detailWaiting').textContent = this.formatElapsed(elapsed);
|
||||||
document.getElementById('detailInfo').textContent = task.TaskDetails || '-';
|
document.getElementById('detailInfo').textContent = task.TaskDetails || '-';
|
||||||
|
|
||||||
detail.style.setProperty('--detail-color', category.color || '#fff');
|
detail.style.setProperty('--detail-color', categoryColor);
|
||||||
overlay.classList.add('visible');
|
overlay.classList.add('visible');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -225,33 +225,40 @@
|
||||||
color: #444;
|
color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Category colors */
|
/* Dynamic color from API (overrides category defaults) */
|
||||||
.task-bar[data-category="1"] {
|
.task-bar {
|
||||||
|
--task-color: var(--bar-color, #888888);
|
||||||
|
--task-color-light: var(--bar-color, #aaaaaa);
|
||||||
|
--task-color-glow: var(--bar-color, rgba(136, 136, 136, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category color fallbacks (used when --bar-color not set) */
|
||||||
|
.task-bar[data-category="1"]:not([style*="--bar-color"]) {
|
||||||
--task-color: #ef4444;
|
--task-color: #ef4444;
|
||||||
--task-color-light: #f87171;
|
--task-color-light: #f87171;
|
||||||
--task-color-glow: rgba(239, 68, 68, 0.3);
|
--task-color-glow: rgba(239, 68, 68, 0.3);
|
||||||
}
|
}
|
||||||
.task-bar[data-category="2"] {
|
.task-bar[data-category="2"]:not([style*="--bar-color"]) {
|
||||||
--task-color: #f59e0b;
|
--task-color: #f59e0b;
|
||||||
--task-color-light: #fbbf24;
|
--task-color-light: #fbbf24;
|
||||||
--task-color-glow: rgba(245, 158, 11, 0.3);
|
--task-color-glow: rgba(245, 158, 11, 0.3);
|
||||||
}
|
}
|
||||||
.task-bar[data-category="3"] {
|
.task-bar[data-category="3"]:not([style*="--bar-color"]) {
|
||||||
--task-color: #22c55e;
|
--task-color: #22c55e;
|
||||||
--task-color-light: #4ade80;
|
--task-color-light: #4ade80;
|
||||||
--task-color-glow: rgba(34, 197, 94, 0.3);
|
--task-color-glow: rgba(34, 197, 94, 0.3);
|
||||||
}
|
}
|
||||||
.task-bar[data-category="4"] {
|
.task-bar[data-category="4"]:not([style*="--bar-color"]) {
|
||||||
--task-color: #3b82f6;
|
--task-color: #3b82f6;
|
||||||
--task-color-light: #60a5fa;
|
--task-color-light: #60a5fa;
|
||||||
--task-color-glow: rgba(59, 130, 246, 0.3);
|
--task-color-glow: rgba(59, 130, 246, 0.3);
|
||||||
}
|
}
|
||||||
.task-bar[data-category="5"] {
|
.task-bar[data-category="5"]:not([style*="--bar-color"]) {
|
||||||
--task-color: #8b5cf6;
|
--task-color: #8b5cf6;
|
||||||
--task-color-light: #a78bfa;
|
--task-color-light: #a78bfa;
|
||||||
--task-color-glow: rgba(139, 92, 246, 0.3);
|
--task-color-glow: rgba(139, 92, 246, 0.3);
|
||||||
}
|
}
|
||||||
.task-bar[data-category="6"] {
|
.task-bar[data-category="6"]:not([style*="--bar-color"]) {
|
||||||
--task-color: #ec4899;
|
--task-color: #ec4899;
|
||||||
--task-color-light: #f472b6;
|
--task-color-light: #f472b6;
|
||||||
--task-color-glow: rgba(236, 72, 153, 0.3);
|
--task-color-glow: rgba(236, 72, 153, 0.3);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Configuration
|
// Configuration
|
||||||
let config = {
|
let config = {
|
||||||
apiBaseUrl: '/biz.payfrit.com/api',
|
apiBaseUrl: 'https://biz.payfrit.com/api',
|
||||||
businessId: null,
|
businessId: null,
|
||||||
servicePointId: null,
|
servicePointId: null,
|
||||||
stationId: null,
|
stationId: null,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue