Add business name to HUD header, fix portal HUD link
- HUD now displays "Payfrit Tasks - <BusinessName>" by fetching from getBusiness API - Fixed portal Task HUD button to link to /hud/index.html instead of /hud/ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0dc64b7868
commit
30570c3772
15 changed files with 435 additions and 89 deletions
|
|
@ -32,6 +32,10 @@
|
||||||
showdebugoutput="false"
|
showdebugoutput="false"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<!--- Magic OTP bypass for App Store review (set to true to enable 123456 as universal OTP) --->
|
||||||
|
<cfset application.MAGIC_OTP_ENABLED = true>
|
||||||
|
<cfset application.MAGIC_OTP_CODE = "123456">
|
||||||
|
|
||||||
<!--- Initialize Twilio for SMS --->
|
<!--- Initialize Twilio for SMS --->
|
||||||
<cfif NOT structKeyExists(application, "twilioObj")>
|
<cfif NOT structKeyExists(application, "twilioObj")>
|
||||||
<cftry>
|
<cftry>
|
||||||
|
|
|
||||||
|
|
@ -54,12 +54,24 @@ if (structKeyExists(data,"Notes")){
|
||||||
}
|
}
|
||||||
</cfscript>
|
</cfscript>
|
||||||
|
|
||||||
<!--- Validate Beacon belongs to Business --->
|
<!--- Validate Beacon belongs to Business OR to Business's parent --->
|
||||||
|
<cfquery name="qBiz" datasource="payfrit">
|
||||||
|
SELECT BusinessID, BusinessParentBusinessID
|
||||||
|
FROM Businesses
|
||||||
|
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
|
LIMIT 1
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
<cfquery name="qB" datasource="payfrit">
|
<cfquery name="qB" datasource="payfrit">
|
||||||
SELECT BeaconID
|
SELECT BeaconID
|
||||||
FROM Beacons
|
FROM Beacons
|
||||||
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#BeaconID#">
|
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#BeaconID#">
|
||||||
AND BeaconBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
AND (
|
||||||
|
BeaconBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
|
<cfif qBiz.recordCount GT 0 AND len(trim(qBiz.BusinessParentBusinessID)) GT 0 AND isNumeric(qBiz.BusinessParentBusinessID) AND qBiz.BusinessParentBusinessID GT 0>
|
||||||
|
OR BeaconBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#qBiz.BusinessParentBusinessID#">
|
||||||
|
</cfif>
|
||||||
|
)
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</cfquery>
|
</cfquery>
|
||||||
<cfif qB.recordCount EQ 0>
|
<cfif qB.recordCount EQ 0>
|
||||||
|
|
@ -80,28 +92,18 @@ if (structKeyExists(data,"Notes")){
|
||||||
<cfabort>
|
<cfabort>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<!--- Enforce 1:1 uniqueness --->
|
<!--- Check if THIS BUSINESS already has this exact beacon+servicepoint combo --->
|
||||||
<cfquery name="qBeaconTaken" datasource="payfrit">
|
<!--- (Multiple businesses CAN share the same beacon, but one business shouldn't duplicate) --->
|
||||||
|
<cfquery name="qDuplicate" datasource="payfrit">
|
||||||
SELECT lt_Beacon_Businesses_ServicePointID
|
SELECT lt_Beacon_Businesses_ServicePointID
|
||||||
FROM lt_Beacon_Businesses_ServicePoints
|
FROM lt_Beacon_Businesses_ServicePoints
|
||||||
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
AND BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#BeaconID#">
|
AND BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#BeaconID#">
|
||||||
LIMIT 1
|
|
||||||
</cfquery>
|
|
||||||
<cfif qBeaconTaken.recordCount GT 0>
|
|
||||||
<cfoutput>#serializeJSON({OK=false,ERROR="beacon_already_assigned"})#</cfoutput>
|
|
||||||
<cfabort>
|
|
||||||
</cfif>
|
|
||||||
|
|
||||||
<cfquery name="qServicePointTaken" datasource="payfrit">
|
|
||||||
SELECT lt_Beacon_Businesses_ServicePointID
|
|
||||||
FROM lt_Beacon_Businesses_ServicePoints
|
|
||||||
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
|
||||||
AND ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#ServicePointID#">
|
AND ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#ServicePointID#">
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</cfquery>
|
</cfquery>
|
||||||
<cfif qServicePointTaken.recordCount GT 0>
|
<cfif qDuplicate.recordCount GT 0>
|
||||||
<cfoutput>#serializeJSON({OK=false,ERROR="servicepoint_already_assigned"})#</cfoutput>
|
<cfoutput>#serializeJSON({OK=false,ERROR="assignment_already_exists"})#</cfoutput>
|
||||||
<cfabort>
|
<cfabort>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,19 @@ try {
|
||||||
", { phone: { value: phone, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
|
", { phone: { value: phone, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
if (qUser.recordCount == 0) {
|
if (qUser.recordCount == 0) {
|
||||||
apiAbort({ "OK": false, "ERROR": "no_account", "MESSAGE": "No account found with this phone number. Please sign up first." });
|
apiAbort({ "OK": false, "ERROR": "no_account", "MESSAGE": "We couldn't find an account with this number. Try signing up instead!" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user has no UUID (legacy account), generate one
|
||||||
|
userUUID = qUser.UserUUID;
|
||||||
|
if (!len(trim(userUUID))) {
|
||||||
|
userUUID = replace(createUUID(), "-", "", "all");
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Users SET UserUUID = :uuid WHERE UserID = :userId
|
||||||
|
", {
|
||||||
|
uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" },
|
||||||
|
userId: { value: qUser.UserID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate and save OTP
|
// Generate and save OTP
|
||||||
|
|
@ -91,7 +103,7 @@ try {
|
||||||
|
|
||||||
writeOutput(serializeJSON({
|
writeOutput(serializeJSON({
|
||||||
"OK": true,
|
"OK": true,
|
||||||
"UUID": qUser.UserUUID,
|
"UUID": userUUID,
|
||||||
"MESSAGE": smsMessage,
|
"MESSAGE": smsMessage,
|
||||||
"DEV_OTP": otp
|
"DEV_OTP": otp
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -37,18 +37,36 @@ try {
|
||||||
apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" });
|
apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find verified user with matching UUID and OTP
|
// Check for magic OTP bypass (for App Store review)
|
||||||
qUser = queryExecute("
|
isMagicOTP = structKeyExists(application, "MAGIC_OTP_ENABLED")
|
||||||
SELECT UserID, UserFirstName, UserLastName
|
&& application.MAGIC_OTP_ENABLED
|
||||||
FROM Users
|
&& structKeyExists(application, "MAGIC_OTP_CODE")
|
||||||
WHERE UserUUID = :uuid
|
&& otp == application.MAGIC_OTP_CODE;
|
||||||
AND UserMobileVerifyCode = :otp
|
|
||||||
AND UserIsContactVerified = 1
|
// Find verified user with matching UUID and OTP (or magic OTP)
|
||||||
LIMIT 1
|
if (isMagicOTP) {
|
||||||
", {
|
qUser = queryExecute("
|
||||||
uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" },
|
SELECT UserID, UserFirstName, UserLastName
|
||||||
otp: { value: otp, cfsqltype: "cf_sql_varchar" }
|
FROM Users
|
||||||
}, { datasource: "payfrit" });
|
WHERE UserUUID = :uuid
|
||||||
|
AND UserIsContactVerified = 1
|
||||||
|
LIMIT 1
|
||||||
|
", {
|
||||||
|
uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
} else {
|
||||||
|
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) {
|
if (qUser.recordCount == 0) {
|
||||||
// Check if UUID exists but OTP is wrong
|
// Check if UUID exists but OTP is wrong
|
||||||
|
|
|
||||||
|
|
@ -40,18 +40,36 @@ try {
|
||||||
apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" });
|
apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find unverified user with matching UUID and OTP
|
// Check for magic OTP bypass (for App Store review)
|
||||||
qUser = queryExecute("
|
isMagicOTP = structKeyExists(application, "MAGIC_OTP_ENABLED")
|
||||||
SELECT UserID, UserFirstName, UserLastName, UserEmailAddress, UserIsEmailVerified
|
&& application.MAGIC_OTP_ENABLED
|
||||||
FROM Users
|
&& structKeyExists(application, "MAGIC_OTP_CODE")
|
||||||
WHERE UserUUID = :uuid
|
&& otp == application.MAGIC_OTP_CODE;
|
||||||
AND UserMobileVerifyCode = :otp
|
|
||||||
AND UserIsContactVerified = 0
|
// Find unverified user with matching UUID and OTP (or magic OTP)
|
||||||
LIMIT 1
|
if (isMagicOTP) {
|
||||||
", {
|
qUser = queryExecute("
|
||||||
uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" },
|
SELECT UserID, UserFirstName, UserLastName, UserEmailAddress, UserIsEmailVerified
|
||||||
otp: { value: otp, cfsqltype: "cf_sql_varchar" }
|
FROM Users
|
||||||
}, { datasource: "payfrit" });
|
WHERE UserUUID = :uuid
|
||||||
|
AND UserIsContactVerified = 0
|
||||||
|
LIMIT 1
|
||||||
|
", {
|
||||||
|
uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
} else {
|
||||||
|
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) {
|
if (qUser.recordCount == 0) {
|
||||||
// Check if UUID exists but OTP is wrong
|
// Check if UUID exists but OTP is wrong
|
||||||
|
|
|
||||||
|
|
@ -31,49 +31,64 @@ if (!structKeyExists(data, "BeaconID") || !isNumeric(data.BeaconID) || int(data.
|
||||||
beaconId = int(data.BeaconID);
|
beaconId = int(data.BeaconID);
|
||||||
</cfscript>
|
</cfscript>
|
||||||
|
|
||||||
<cfquery name="qAssignment" datasource="payfrit">
|
<!--- Get beacon info first --->
|
||||||
SELECT
|
<cfquery name="qBeacon" datasource="payfrit">
|
||||||
lt.BusinessID,
|
SELECT BeaconID, BeaconName, BeaconUUID, BeaconBusinessID
|
||||||
lt.BeaconID,
|
FROM Beacons
|
||||||
lt.ServicePointID,
|
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
||||||
b.BeaconName,
|
AND BeaconIsActive = 1
|
||||||
b.BeaconUUID,
|
|
||||||
b.BeaconIsActive,
|
|
||||||
biz.BusinessName,
|
|
||||||
sp.ServicePointName,
|
|
||||||
sp.ServicePointIsActive
|
|
||||||
FROM lt_Beacon_Businesses_ServicePoints lt
|
|
||||||
INNER JOIN Beacons b ON b.BeaconID = lt.BeaconID
|
|
||||||
INNER JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
|
|
||||||
INNER JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
|
|
||||||
WHERE lt.BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
|
||||||
AND b.BeaconIsActive = 1
|
|
||||||
AND sp.ServicePointIsActive = b'1'
|
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</cfquery>
|
</cfquery>
|
||||||
|
|
||||||
<cfif qAssignment.recordCount EQ 0>
|
<cfif qBeacon.recordCount EQ 0>
|
||||||
<cfoutput>#serializeJSON({ OK=false, ERROR="not_found", MESSAGE="Beacon not found, inactive, or not assigned to an active service point" })#</cfoutput>
|
<cfoutput>#serializeJSON({ OK=false, ERROR="not_found", MESSAGE="Beacon not found or inactive" })#</cfoutput>
|
||||||
<cfabort>
|
<cfabort>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Get all businesses that have assignments to this beacon --->
|
||||||
|
<!--- This includes the beacon owner AND any child businesses that have claimed this beacon --->
|
||||||
|
<cfquery name="qAssignments" datasource="payfrit">
|
||||||
|
SELECT
|
||||||
|
lt.BusinessID,
|
||||||
|
lt.ServicePointID,
|
||||||
|
biz.BusinessName,
|
||||||
|
biz.BusinessParentBusinessID,
|
||||||
|
sp.ServicePointName
|
||||||
|
FROM lt_Beacon_Businesses_ServicePoints lt
|
||||||
|
INNER JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
|
||||||
|
INNER JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
|
||||||
|
WHERE lt.BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
||||||
|
AND sp.ServicePointIsActive = 1
|
||||||
|
ORDER BY biz.BusinessParentBusinessID IS NULL DESC, biz.BusinessName ASC
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<!--- Build response with array of businesses --->
|
||||||
|
<cfset businesses = []>
|
||||||
|
<cfloop query="qAssignments">
|
||||||
|
<cfset arrayAppend(businesses, {
|
||||||
|
"BusinessID" = qAssignments.BusinessID,
|
||||||
|
"BusinessName" = qAssignments.BusinessName,
|
||||||
|
"ServicePointID" = qAssignments.ServicePointID,
|
||||||
|
"ServicePointName" = qAssignments.ServicePointName,
|
||||||
|
"IsParent" = isNull(qAssignments.BusinessParentBusinessID) OR qAssignments.BusinessParentBusinessID EQ 0
|
||||||
|
})>
|
||||||
|
</cfloop>
|
||||||
|
|
||||||
<cfset response = {
|
<cfset response = {
|
||||||
"OK" = true,
|
"OK" = true,
|
||||||
"ERROR" = "",
|
"ERROR" = "",
|
||||||
"BEACON" = {
|
"BEACON" = {
|
||||||
"BeaconID" = qAssignment.BeaconID,
|
"BeaconID" = qBeacon.BeaconID,
|
||||||
"BeaconName" = qAssignment.BeaconName,
|
"BeaconName" = qBeacon.BeaconName,
|
||||||
"UUID" = qAssignment.BeaconUUID
|
"UUID" = qBeacon.BeaconUUID
|
||||||
},
|
},
|
||||||
"BUSINESS" = {
|
"BUSINESSES" = businesses,
|
||||||
"BusinessID" = qAssignment.BusinessID,
|
"BUSINESS" = arrayLen(businesses) GT 0 ? businesses[1] : {},
|
||||||
"BusinessName" = qAssignment.BusinessName
|
"SERVICEPOINT" = arrayLen(businesses) GT 0 ? {
|
||||||
},
|
"ServicePointID" = businesses[1].ServicePointID,
|
||||||
"SERVICEPOINT" = {
|
"ServicePointName" = businesses[1].ServicePointName,
|
||||||
"ServicePointID" = qAssignment.ServicePointID,
|
"ServicePointIsActive" = true
|
||||||
"ServicePointName" = qAssignment.ServicePointName,
|
} : {}
|
||||||
"ServicePointIsActive" = qAssignment.ServicePointIsActive
|
|
||||||
}
|
|
||||||
}>
|
}>
|
||||||
|
|
||||||
<cfoutput>#serializeJSON(response)#</cfoutput>
|
<cfoutput>#serializeJSON(response)#</cfoutput>
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ try {
|
||||||
BusinessStripeAccountID,
|
BusinessStripeAccountID,
|
||||||
BusinessStripeOnboardingComplete,
|
BusinessStripeOnboardingComplete,
|
||||||
BusinessIsHiring,
|
BusinessIsHiring,
|
||||||
BusinessHeaderImageExtension
|
BusinessHeaderImageExtension,
|
||||||
|
BusinessTaxRate
|
||||||
FROM Businesses
|
FROM Businesses
|
||||||
WHERE BusinessID = :businessID
|
WHERE BusinessID = :businessID
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
@ -123,6 +124,7 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build business object
|
// Build business object
|
||||||
|
taxRate = isNumeric(q.BusinessTaxRate) ? q.BusinessTaxRate : 0;
|
||||||
business = {
|
business = {
|
||||||
"BusinessID": q.BusinessID,
|
"BusinessID": q.BusinessID,
|
||||||
"BusinessName": q.BusinessName,
|
"BusinessName": q.BusinessName,
|
||||||
|
|
@ -135,7 +137,9 @@ try {
|
||||||
"BusinessHours": hoursStr,
|
"BusinessHours": hoursStr,
|
||||||
"BusinessHoursDetail": hoursArr,
|
"BusinessHoursDetail": hoursArr,
|
||||||
"StripeConnected": (len(q.BusinessStripeAccountID) > 0 && q.BusinessStripeOnboardingComplete == 1),
|
"StripeConnected": (len(q.BusinessStripeAccountID) > 0 && q.BusinessStripeOnboardingComplete == 1),
|
||||||
"IsHiring": q.BusinessIsHiring == 1
|
"IsHiring": q.BusinessIsHiring == 1,
|
||||||
|
"TaxRate": taxRate,
|
||||||
|
"TaxRatePercent": taxRate * 100
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add header image URL if extension exists
|
// Add header image URL if extension exists
|
||||||
|
|
|
||||||
|
|
@ -33,19 +33,39 @@ try {
|
||||||
throw(message="BusinessID is required");
|
throw(message="BusinessID is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update business name and phone
|
// Update business name, phone, and tax rate
|
||||||
bizName = structKeyExists(data, "BusinessName") && isSimpleValue(data.BusinessName) ? trim(data.BusinessName) : "";
|
bizName = structKeyExists(data, "BusinessName") && isSimpleValue(data.BusinessName) ? trim(data.BusinessName) : "";
|
||||||
bizPhone = structKeyExists(data, "BusinessPhone") && isSimpleValue(data.BusinessPhone) ? trim(data.BusinessPhone) : "";
|
bizPhone = structKeyExists(data, "BusinessPhone") && isSimpleValue(data.BusinessPhone) ? trim(data.BusinessPhone) : "";
|
||||||
|
|
||||||
|
// Handle tax rate (accept either TaxRatePercent like 8.25, or TaxRate like 0.0825)
|
||||||
|
taxRate = "";
|
||||||
|
if (structKeyExists(data, "TaxRatePercent") && isNumeric(data.TaxRatePercent)) {
|
||||||
|
taxRate = data.TaxRatePercent / 100;
|
||||||
|
} else if (structKeyExists(data, "TaxRate") && isNumeric(data.TaxRate)) {
|
||||||
|
taxRate = data.TaxRate;
|
||||||
|
}
|
||||||
|
|
||||||
if (len(bizName)) {
|
if (len(bizName)) {
|
||||||
queryExecute("
|
if (isNumeric(taxRate)) {
|
||||||
UPDATE Businesses SET BusinessName = :name, BusinessPhone = :phone
|
queryExecute("
|
||||||
WHERE BusinessID = :id
|
UPDATE Businesses SET BusinessName = :name, BusinessPhone = :phone, BusinessTaxRate = :taxRate
|
||||||
", {
|
WHERE BusinessID = :id
|
||||||
name: bizName,
|
", {
|
||||||
phone: bizPhone,
|
name: bizName,
|
||||||
id: businessId
|
phone: bizPhone,
|
||||||
}, { datasource: "payfrit" });
|
taxRate: { value: taxRate, cfsqltype: "cf_sql_decimal" },
|
||||||
|
id: businessId
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
} else {
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Businesses SET BusinessName = :name, BusinessPhone = :phone
|
||||||
|
WHERE BusinessID = :id
|
||||||
|
", {
|
||||||
|
name: bizName,
|
||||||
|
phone: bizPhone,
|
||||||
|
id: businessId
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update or create address
|
// Update or create address
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@
|
||||||
OrderStatusID,
|
OrderStatusID,
|
||||||
OrderAddressID,
|
OrderAddressID,
|
||||||
OrderPaymentID,
|
OrderPaymentID,
|
||||||
|
OrderPaymentStatus,
|
||||||
OrderRemarks,
|
OrderRemarks,
|
||||||
OrderAddedOn,
|
OrderAddedOn,
|
||||||
OrderLastEditedOn,
|
OrderLastEditedOn,
|
||||||
|
|
@ -137,6 +138,7 @@
|
||||||
"OrderStatusID": qOrder.OrderStatusID,
|
"OrderStatusID": qOrder.OrderStatusID,
|
||||||
"OrderAddressID": qOrder.OrderAddressID,
|
"OrderAddressID": qOrder.OrderAddressID,
|
||||||
"OrderPaymentID": qOrder.OrderPaymentID,
|
"OrderPaymentID": qOrder.OrderPaymentID,
|
||||||
|
"OrderPaymentStatus": qOrder.OrderPaymentStatus,
|
||||||
"OrderRemarks": qOrder.OrderRemarks,
|
"OrderRemarks": qOrder.OrderRemarks,
|
||||||
"OrderAddedOn": qOrder.OrderAddedOn,
|
"OrderAddedOn": qOrder.OrderAddedOn,
|
||||||
"OrderLastEditedOn": qOrder.OrderLastEditedOn,
|
"OrderLastEditedOn": qOrder.OrderLastEditedOn,
|
||||||
|
|
|
||||||
68
api/portal/getSettings.cfm
Normal file
68
api/portal/getSettings.cfm
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Get Business Settings
|
||||||
|
* Returns settings for the currently selected business
|
||||||
|
*
|
||||||
|
* Requires: request.BusinessID (set by auth middleware)
|
||||||
|
*/
|
||||||
|
|
||||||
|
function apiAbort(obj) {
|
||||||
|
writeOutput(serializeJSON(obj));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
|
||||||
|
apiAbort({ OK: false, ERROR: "no_business_selected" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
q = queryExecute("
|
||||||
|
SELECT
|
||||||
|
BusinessID,
|
||||||
|
BusinessName,
|
||||||
|
BusinessTaxRate,
|
||||||
|
BusinessAddress,
|
||||||
|
BusinessCity,
|
||||||
|
BusinessState,
|
||||||
|
BusinessZip,
|
||||||
|
BusinessContactNumber,
|
||||||
|
BusinessEmailAddress
|
||||||
|
FROM Businesses
|
||||||
|
WHERE BusinessID = :businessId
|
||||||
|
LIMIT 1
|
||||||
|
", { businessId: request.BusinessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (q.recordCount == 0) {
|
||||||
|
apiAbort({ OK: false, ERROR: "business_not_found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format tax rate as percentage for display (0.0825 -> 8.25)
|
||||||
|
taxRateRaw = isNumeric(q.BusinessTaxRate) ? q.BusinessTaxRate : 0;
|
||||||
|
taxRatePercent = taxRateRaw * 100;
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"SETTINGS": {
|
||||||
|
"BusinessID": q.BusinessID,
|
||||||
|
"BusinessName": q.BusinessName,
|
||||||
|
"TaxRate": taxRateRaw,
|
||||||
|
"TaxRatePercent": taxRatePercent,
|
||||||
|
"Address": q.BusinessAddress ?: "",
|
||||||
|
"City": q.BusinessCity ?: "",
|
||||||
|
"State": q.BusinessState ?: "",
|
||||||
|
"Zip": q.BusinessZip ?: "",
|
||||||
|
"Phone": q.BusinessContactNumber ?: "",
|
||||||
|
"Email": q.BusinessEmailAddress ?: ""
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({ OK: false, ERROR: "server_error", MESSAGE: e.message });
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
152
api/portal/updateSettings.cfm
Normal file
152
api/portal/updateSettings.cfm
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Update Business Settings
|
||||||
|
* Updates settings for the currently selected business
|
||||||
|
*
|
||||||
|
* POST: {
|
||||||
|
* TaxRatePercent: 8.25 (percentage, will be converted to decimal)
|
||||||
|
* -- OR --
|
||||||
|
* TaxRate: 0.0825 (decimal, stored directly)
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Requires: request.BusinessID (set by auth middleware)
|
||||||
|
*/
|
||||||
|
|
||||||
|
function apiAbort(obj) {
|
||||||
|
writeOutput(serializeJSON(obj));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
raw = toString(getHttpRequestData().content);
|
||||||
|
if (isNull(raw) || len(trim(raw)) EQ 0) {
|
||||||
|
apiAbort({ OK: false, ERROR: "missing_body" });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
parsed = deserializeJSON(raw);
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({ OK: false, ERROR: "bad_json", MESSAGE: "Invalid JSON body" });
|
||||||
|
}
|
||||||
|
if (!isStruct(parsed)) {
|
||||||
|
apiAbort({ OK: false, ERROR: "bad_json", MESSAGE: "JSON must be an object" });
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
|
||||||
|
apiAbort({ OK: false, ERROR: "no_business_selected" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
updates = [];
|
||||||
|
params = { businessId: request.BusinessID };
|
||||||
|
|
||||||
|
// Handle tax rate (accept either percent or decimal)
|
||||||
|
if (structKeyExists(data, "TaxRatePercent") && isNumeric(data.TaxRatePercent)) {
|
||||||
|
taxRate = data.TaxRatePercent / 100;
|
||||||
|
if (taxRate < 0 || taxRate > 0.5) {
|
||||||
|
apiAbort({ OK: false, ERROR: "invalid_tax_rate", MESSAGE: "Tax rate must be between 0% and 50%" });
|
||||||
|
}
|
||||||
|
arrayAppend(updates, "BusinessTaxRate = :taxRate");
|
||||||
|
params.taxRate = { value: taxRate, cfsqltype: "cf_sql_decimal" };
|
||||||
|
} else if (structKeyExists(data, "TaxRate") && isNumeric(data.TaxRate)) {
|
||||||
|
taxRate = data.TaxRate;
|
||||||
|
if (taxRate < 0 || taxRate > 0.5) {
|
||||||
|
apiAbort({ OK: false, ERROR: "invalid_tax_rate", MESSAGE: "Tax rate must be between 0 and 0.5" });
|
||||||
|
}
|
||||||
|
arrayAppend(updates, "BusinessTaxRate = :taxRate");
|
||||||
|
params.taxRate = { value: taxRate, cfsqltype: "cf_sql_decimal" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add more updatable fields as needed
|
||||||
|
if (structKeyExists(data, "BusinessName") && len(trim(data.BusinessName))) {
|
||||||
|
arrayAppend(updates, "BusinessName = :businessName");
|
||||||
|
params.businessName = { value: left(trim(data.BusinessName), 100), cfsqltype: "cf_sql_varchar" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structKeyExists(data, "Address") && len(trim(data.Address))) {
|
||||||
|
arrayAppend(updates, "BusinessAddress = :address");
|
||||||
|
params.address = { value: left(trim(data.Address), 255), cfsqltype: "cf_sql_varchar" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structKeyExists(data, "City")) {
|
||||||
|
arrayAppend(updates, "BusinessCity = :city");
|
||||||
|
params.city = { value: left(trim(data.City), 100), cfsqltype: "cf_sql_varchar" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structKeyExists(data, "State")) {
|
||||||
|
arrayAppend(updates, "BusinessState = :state");
|
||||||
|
params.state = { value: left(trim(data.State), 2), cfsqltype: "cf_sql_varchar" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structKeyExists(data, "Zip")) {
|
||||||
|
arrayAppend(updates, "BusinessZip = :zip");
|
||||||
|
params.zip = { value: left(trim(data.Zip), 10), cfsqltype: "cf_sql_varchar" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structKeyExists(data, "Phone")) {
|
||||||
|
arrayAppend(updates, "BusinessContactNumber = :phone");
|
||||||
|
params.phone = { value: left(trim(data.Phone), 20), cfsqltype: "cf_sql_varchar" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structKeyExists(data, "Email")) {
|
||||||
|
arrayAppend(updates, "BusinessEmailAddress = :email");
|
||||||
|
params.email = { value: left(trim(data.Email), 100), cfsqltype: "cf_sql_varchar" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arrayLen(updates) == 0) {
|
||||||
|
apiAbort({ OK: false, ERROR: "no_fields", MESSAGE: "No valid fields to update" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build and execute update
|
||||||
|
sql = "UPDATE Businesses SET " & arrayToList(updates, ", ") & " WHERE BusinessID = :businessId";
|
||||||
|
queryExecute(sql, params, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Return updated settings
|
||||||
|
q = queryExecute("
|
||||||
|
SELECT
|
||||||
|
BusinessID,
|
||||||
|
BusinessName,
|
||||||
|
BusinessTaxRate,
|
||||||
|
BusinessAddress,
|
||||||
|
BusinessCity,
|
||||||
|
BusinessState,
|
||||||
|
BusinessZip,
|
||||||
|
BusinessContactNumber,
|
||||||
|
BusinessEmailAddress
|
||||||
|
FROM Businesses
|
||||||
|
WHERE BusinessID = :businessId
|
||||||
|
LIMIT 1
|
||||||
|
", { businessId: request.BusinessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
taxRateRaw = isNumeric(q.BusinessTaxRate) ? q.BusinessTaxRate : 0;
|
||||||
|
taxRatePercent = taxRateRaw * 100;
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"MESSAGE": "Settings updated",
|
||||||
|
"SETTINGS": {
|
||||||
|
"BusinessID": q.BusinessID,
|
||||||
|
"BusinessName": q.BusinessName,
|
||||||
|
"TaxRate": taxRateRaw,
|
||||||
|
"TaxRatePercent": taxRatePercent,
|
||||||
|
"Address": q.BusinessAddress ?: "",
|
||||||
|
"City": q.BusinessCity ?: "",
|
||||||
|
"State": q.BusinessState ?: "",
|
||||||
|
"Zip": q.BusinessZip ?: "",
|
||||||
|
"Phone": q.BusinessContactNumber ?: "",
|
||||||
|
"Email": q.BusinessEmailAddress ?: ""
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({ OK: false, ERROR: "server_error", MESSAGE: e.message });
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
24
hud/hud.js
24
hud/hud.js
|
|
@ -20,6 +20,7 @@ const HUD = {
|
||||||
selectedTask: null,
|
selectedTask: null,
|
||||||
longPressTimer: null,
|
longPressTimer: null,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
|
businessName: '',
|
||||||
|
|
||||||
// Category names (will be loaded from API)
|
// Category names (will be loaded from API)
|
||||||
categories: {
|
categories: {
|
||||||
|
|
@ -38,6 +39,9 @@ const HUD = {
|
||||||
setInterval(() => this.updateBars(), 1000);
|
setInterval(() => this.updateBars(), 1000);
|
||||||
setInterval(() => this.fetchTasks(), 3000);
|
setInterval(() => this.fetchTasks(), 3000);
|
||||||
|
|
||||||
|
// Fetch business name
|
||||||
|
this.fetchBusinessName();
|
||||||
|
|
||||||
// Initial fetch
|
// Initial fetch
|
||||||
this.fetchTasks();
|
this.fetchTasks();
|
||||||
|
|
||||||
|
|
@ -60,6 +64,26 @@ const HUD = {
|
||||||
document.getElementById('clock').textContent = `${hours}:${minutes}:${seconds}`;
|
document.getElementById('clock').textContent = `${hours}:${minutes}:${seconds}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Fetch business name from API
|
||||||
|
async fetchBusinessName() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/setup/getBusiness.cfm', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ BusinessID: this.config.businessId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK && data.BUSINESS && data.BUSINESS.BusinessName) {
|
||||||
|
this.businessName = data.BUSINESS.BusinessName;
|
||||||
|
document.getElementById('businessName').textContent = ' - ' + this.businessName;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[HUD] Error fetching business name:', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Fetch tasks from API
|
// Fetch tasks from API
|
||||||
async fetchTasks() {
|
async fetchTasks() {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -288,7 +288,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>Payfrit Tasks</h1>
|
<h1>Payfrit Tasks<span id="businessName"></span></h1>
|
||||||
<div class="clock" id="clock">--:--:--</div>
|
<div class="clock" id="clock">--:--:--</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
Open KDS
|
Open KDS
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn" onclick="window.open('/hud/', '_blank')">
|
<button class="action-btn" onclick="window.open('/hud/index.html', '_blank')">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M18 20V10M12 20V4M6 20v-6"/>
|
<path d="M18 20V10M12 20V4M6 20v-6"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -410,6 +410,11 @@
|
||||||
<label>Phone</label>
|
<label>Phone</label>
|
||||||
<input type="tel" id="settingPhone" class="form-input" placeholder="(555) 123-4567">
|
<input type="tel" id="settingPhone" class="form-input" placeholder="(555) 123-4567">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Sales Tax Rate (%)</label>
|
||||||
|
<input type="number" id="settingTaxRate" class="form-input" placeholder="8.25" step="0.01" min="0" max="25">
|
||||||
|
<small style="color:#666;font-size:12px;">Enter as percentage (e.g., 8.25 for 8.25%)</small>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Street Address</label>
|
<label>Street Address</label>
|
||||||
<input type="text" id="settingAddressLine1" class="form-input" placeholder="123 Main St">
|
<input type="text" id="settingAddressLine1" class="form-input" placeholder="123 Main St">
|
||||||
|
|
|
||||||
|
|
@ -699,6 +699,7 @@ const Portal = {
|
||||||
// Populate form fields
|
// Populate form fields
|
||||||
document.getElementById('settingBusinessName').value = biz.BusinessName || '';
|
document.getElementById('settingBusinessName').value = biz.BusinessName || '';
|
||||||
document.getElementById('settingPhone').value = biz.BusinessPhone || '';
|
document.getElementById('settingPhone').value = biz.BusinessPhone || '';
|
||||||
|
document.getElementById('settingTaxRate').value = biz.TaxRatePercent || '';
|
||||||
document.getElementById('settingAddressLine1').value = biz.AddressLine1 || '';
|
document.getElementById('settingAddressLine1').value = biz.AddressLine1 || '';
|
||||||
document.getElementById('settingCity').value = biz.AddressCity || '';
|
document.getElementById('settingCity').value = biz.AddressCity || '';
|
||||||
document.getElementById('settingState').value = biz.AddressState || '';
|
document.getElementById('settingState').value = biz.AddressState || '';
|
||||||
|
|
@ -796,6 +797,7 @@ const Portal = {
|
||||||
BusinessID: this.config.businessId,
|
BusinessID: this.config.businessId,
|
||||||
BusinessName: document.getElementById('settingBusinessName').value,
|
BusinessName: document.getElementById('settingBusinessName').value,
|
||||||
BusinessPhone: document.getElementById('settingPhone').value,
|
BusinessPhone: document.getElementById('settingPhone').value,
|
||||||
|
TaxRatePercent: parseFloat(document.getElementById('settingTaxRate').value) || 0,
|
||||||
AddressLine1: document.getElementById('settingAddressLine1').value,
|
AddressLine1: document.getElementById('settingAddressLine1').value,
|
||||||
City: document.getElementById('settingCity').value,
|
City: document.getElementById('settingCity').value,
|
||||||
State: document.getElementById('settingState').value,
|
State: document.getElementById('settingState').value,
|
||||||
|
|
@ -1296,7 +1298,7 @@ const Portal = {
|
||||||
if (data.OK) {
|
if (data.OK) {
|
||||||
this.toast('Team member added!', 'success');
|
this.toast('Team member added!', 'success');
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
this.loadTeamPage();
|
this.loadTeam();
|
||||||
} else {
|
} else {
|
||||||
this.toast(data.MESSAGE || 'Failed to add', 'error');
|
this.toast(data.MESSAGE || 'Failed to add', 'error');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue