Launch prep: fix menu builder, payment flow, comment out pre-launch features
- Fix menu builder dropdown showing empty names (return MenuName instead of Name) - Add default menu selection (setDefault action, DefaultMenuID in getForBuilder) - Fix createPaymentIntent column names for dev schema (ID, StripeAccountID, etc.) - Fix menu-builder favicon and remove redundant business label - Comment out Tabs/Running Checks feature for launch (HTML + JS) - Comment out Service Point Marketing/Grants feature for launch (HTML + JS) - Add testMarkPaid.cfm for testing orders without Stripe webhooks - Task API updates for worker payout ledger integration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d7632c5d35
commit
31a89018f5
12 changed files with 417 additions and 281 deletions
|
|
@ -60,6 +60,16 @@ try {
|
||||||
// Check for MenuID filter (optional - if provided, only return categories for that menu)
|
// Check for MenuID filter (optional - if provided, only return categories for that menu)
|
||||||
menuID = structKeyExists(requestData, "MenuID") ? val(requestData.MenuID) : 0;
|
menuID = structKeyExists(requestData, "MenuID") ? val(requestData.MenuID) : 0;
|
||||||
|
|
||||||
|
// Get business's default menu setting
|
||||||
|
defaultMenuID = 0;
|
||||||
|
try {
|
||||||
|
qBiz = queryTimed("SELECT DefaultMenuID FROM Businesses WHERE ID = :businessID",
|
||||||
|
{ businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
if (qBiz.recordCount > 0 && !isNull(qBiz.DefaultMenuID)) {
|
||||||
|
defaultMenuID = val(qBiz.DefaultMenuID);
|
||||||
|
}
|
||||||
|
} catch (any e) {}
|
||||||
|
|
||||||
// Get all menus for this business
|
// Get all menus for this business
|
||||||
allMenus = [];
|
allMenus = [];
|
||||||
try {
|
try {
|
||||||
|
|
@ -74,11 +84,11 @@ try {
|
||||||
for (m = 1; m <= qMenus.recordCount; m++) {
|
for (m = 1; m <= qMenus.recordCount; m++) {
|
||||||
arrayAppend(allMenus, {
|
arrayAppend(allMenus, {
|
||||||
"MenuID": qMenus.ID[m],
|
"MenuID": qMenus.ID[m],
|
||||||
"Name": qMenus.Name[m],
|
"MenuName": qMenus.Name[m],
|
||||||
"Description": isNull(qMenus.Description[m]) ? "" : qMenus.Description[m],
|
"MenuDescription": isNull(qMenus.Description[m]) ? "" : qMenus.Description[m],
|
||||||
"DaysActive": qMenus.DaysActive[m],
|
"MenuDaysActive": qMenus.DaysActive[m],
|
||||||
"StartTime": isNull(qMenus.StartTime[m]) ? "" : timeFormat(qMenus.StartTime[m], "HH:mm"),
|
"MenuStartTime": isNull(qMenus.StartTime[m]) ? "" : timeFormat(qMenus.StartTime[m], "HH:mm"),
|
||||||
"EndTime": isNull(qMenus.EndTime[m]) ? "" : timeFormat(qMenus.EndTime[m], "HH:mm"),
|
"MenuEndTime": isNull(qMenus.EndTime[m]) ? "" : timeFormat(qMenus.EndTime[m], "HH:mm"),
|
||||||
"SortOrder": qMenus.SortOrder[m]
|
"SortOrder": qMenus.SortOrder[m]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -113,8 +123,11 @@ try {
|
||||||
// If exactly one menu is active now, auto-select it
|
// If exactly one menu is active now, auto-select it
|
||||||
if (arrayLen(activeMenuIds) == 1) {
|
if (arrayLen(activeMenuIds) == 1) {
|
||||||
menuID = activeMenuIds[1];
|
menuID = activeMenuIds[1];
|
||||||
|
} else if (arrayLen(activeMenuIds) > 1 && defaultMenuID > 0 && arrayFind(activeMenuIds, defaultMenuID)) {
|
||||||
|
// Multiple menus active - use business default if it's among the active ones
|
||||||
|
menuID = defaultMenuID;
|
||||||
}
|
}
|
||||||
// If multiple match (overlap) or none match, show all (menuID stays 0)
|
// If multiple match with no default, or none match, show all (menuID stays 0)
|
||||||
}
|
}
|
||||||
} catch (any e) {
|
} catch (any e) {
|
||||||
// Menus table might not exist yet
|
// Menus table might not exist yet
|
||||||
|
|
@ -455,6 +468,7 @@ try {
|
||||||
response["MENU"] = { "categories": categories };
|
response["MENU"] = { "categories": categories };
|
||||||
response["MENUS"] = allMenus;
|
response["MENUS"] = allMenus;
|
||||||
response["SELECTED_MENU_ID"] = menuID;
|
response["SELECTED_MENU_ID"] = menuID;
|
||||||
|
response["DEFAULT_MENU_ID"] = defaultMenuID;
|
||||||
response["TEMPLATES"] = templateLibrary;
|
response["TEMPLATES"] = templateLibrary;
|
||||||
response["BRANDCOLOR"] = brandColor;
|
response["BRANDCOLOR"] = brandColor;
|
||||||
response["CATEGORY_COUNT"] = arrayLen(categories);
|
response["CATEGORY_COUNT"] = arrayLen(categories);
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,32 @@ try {
|
||||||
response = { "OK": true, "ACTION": "reordered" };
|
response = { "OK": true, "ACTION": "reordered" };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "setDefault":
|
||||||
|
// Set the default menu for this business
|
||||||
|
menuID = structKeyExists(requestData, "MenuID") ? val(requestData.MenuID) : 0;
|
||||||
|
// MenuID of 0 means "no default" (clear the default)
|
||||||
|
|
||||||
|
if (menuID > 0) {
|
||||||
|
// Verify the menu exists and belongs to this business
|
||||||
|
qCheck = queryTimed("
|
||||||
|
SELECT ID FROM Menus WHERE ID = :menuID AND BusinessID = :businessID AND IsActive = 1
|
||||||
|
", { menuID: menuID, businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qCheck.recordCount == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "invalid_menu", "MESSAGE": "Menu not found or not active" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queryTimed("
|
||||||
|
UPDATE Businesses SET DefaultMenuID = :menuID WHERE ID = :businessID
|
||||||
|
", {
|
||||||
|
menuID: { value: menuID, cfsqltype: "cf_sql_integer", null: menuID == 0 },
|
||||||
|
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response = { "OK": true, "ACTION": "defaultSet", "DefaultMenuID": menuID };
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
apiAbort({ "OK": false, "ERROR": "invalid_action", "MESSAGE": "Unknown action: " & action });
|
apiAbort({ "OK": false, "ERROR": "invalid_action", "MESSAGE": "Unknown action: " & action });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
api/orders/testMarkPaid.cfm
Normal file
55
api/orders/testMarkPaid.cfm
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* TEST ONLY: Mark an order as submitted and paid without Stripe
|
||||||
|
* This bypasses the payment flow for development testing
|
||||||
|
*
|
||||||
|
* POST: { OrderID: int }
|
||||||
|
*/
|
||||||
|
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestData = deserializeJSON(toString(getHttpRequestData().content));
|
||||||
|
orderID = val(requestData.OrderID ?: 0);
|
||||||
|
|
||||||
|
if (orderID == 0) {
|
||||||
|
response["ERROR"] = "OrderID is required";
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check order exists
|
||||||
|
qOrder = queryExecute("
|
||||||
|
SELECT ID, StatusID, PaymentStatus FROM Orders WHERE ID = :orderID
|
||||||
|
", { orderID: orderID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qOrder.recordCount == 0) {
|
||||||
|
response["ERROR"] = "Order not found";
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as submitted and paid
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Orders
|
||||||
|
SET StatusID = 1,
|
||||||
|
PaymentStatus = 'paid',
|
||||||
|
PaymentCompletedOn = NOW(),
|
||||||
|
SubmittedOn = COALESCE(SubmittedOn, NOW()),
|
||||||
|
LastEditedOn = NOW()
|
||||||
|
WHERE ID = :orderID
|
||||||
|
", { orderID: orderID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response["OK"] = true;
|
||||||
|
response["MESSAGE"] = "Order #orderID# marked as submitted and paid";
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response["ERROR"] = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
|
|
@ -62,9 +62,9 @@ try {
|
||||||
|
|
||||||
// Get business Stripe account
|
// Get business Stripe account
|
||||||
qBusiness = queryExecute("
|
qBusiness = queryExecute("
|
||||||
SELECT StripeAccountID AS BusinessStripeAccountID, StripeOnboardingComplete AS BusinessStripeOnboardingComplete, Name AS BusinessName
|
SELECT StripeAccountID, StripeOnboardingComplete, Name
|
||||||
FROM Businesses
|
FROM Businesses
|
||||||
WHERE BusinessID = :businessID
|
WHERE ID = :businessID
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
if (qBusiness.recordCount == 0) {
|
if (qBusiness.recordCount == 0) {
|
||||||
|
|
@ -103,7 +103,7 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
// For testing, allow orders even without Stripe Connect setup
|
// For testing, allow orders even without Stripe Connect setup
|
||||||
hasStripeConnect = qBusiness.BusinessStripeOnboardingComplete == 1 && len(trim(qBusiness.BusinessStripeAccountID)) > 0;
|
hasStripeConnect = qBusiness.StripeOnboardingComplete == 1 && len(trim(qBusiness.StripeAccountID)) > 0;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// FEE CALCULATION
|
// FEE CALCULATION
|
||||||
|
|
@ -138,7 +138,7 @@ try {
|
||||||
// SP-SM: Add grant owner fee to platform fee (Payfrit collects, then transfers to owner)
|
// SP-SM: Add grant owner fee to platform fee (Payfrit collects, then transfers to owner)
|
||||||
effectivePlatformFeeCents = totalPlatformFeeCents + grantOwnerFeeCents;
|
effectivePlatformFeeCents = totalPlatformFeeCents + grantOwnerFeeCents;
|
||||||
httpService.addParam(type="formfield", name="application_fee_amount", value=effectivePlatformFeeCents);
|
httpService.addParam(type="formfield", name="application_fee_amount", value=effectivePlatformFeeCents);
|
||||||
httpService.addParam(type="formfield", name="transfer_data[destination]", value=qBusiness.BusinessStripeAccountID);
|
httpService.addParam(type="formfield", name="transfer_data[destination]", value=qBusiness.StripeAccountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
httpService.addParam(type="formfield", name="metadata[order_id]", value=orderID);
|
httpService.addParam(type="formfield", name="metadata[order_id]", value=orderID);
|
||||||
|
|
@ -150,7 +150,7 @@ try {
|
||||||
httpService.addParam(type="formfield", name="metadata[grant_owner_business_id]", value=grantOwnerBusinessID);
|
httpService.addParam(type="formfield", name="metadata[grant_owner_business_id]", value=grantOwnerBusinessID);
|
||||||
httpService.addParam(type="formfield", name="metadata[grant_owner_fee_cents]", value=grantOwnerFeeCents);
|
httpService.addParam(type="formfield", name="metadata[grant_owner_fee_cents]", value=grantOwnerFeeCents);
|
||||||
}
|
}
|
||||||
httpService.addParam(type="formfield", name="description", value="Order ###orderID# at #qBusiness.BusinessName#");
|
httpService.addParam(type="formfield", name="description", value="Order ###orderID# at #qBusiness.Name#");
|
||||||
|
|
||||||
if (customerEmail != "") {
|
if (customerEmail != "") {
|
||||||
httpService.addParam(type="formfield", name="receipt_email", value=customerEmail);
|
httpService.addParam(type="formfield", name="receipt_email", value=customerEmail);
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
|
|
||||||
<!--- Optional: Worker rating of customer (when required or voluntary) --->
|
<!--- Optional: Worker rating of customer (when required or voluntary) --->
|
||||||
<cfset workerRating = structKeyExists(data,"workerRating") ? data.workerRating : {}>
|
<cfset workerRating = structKeyExists(data,"workerRating") ? data.workerRating : {}>
|
||||||
|
<cfset CashReceivedCents = val( structKeyExists(data,"CashReceivedCents") ? data.CashReceivedCents : 0 )>
|
||||||
|
|
||||||
<cfif TaskID LTE 0>
|
<cfif TaskID LTE 0>
|
||||||
<cfset apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "TaskID is required." })>
|
<cfset apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "TaskID is required." })>
|
||||||
|
|
@ -46,9 +47,13 @@
|
||||||
<!--- Verify task exists --->
|
<!--- Verify task exists --->
|
||||||
<cfset qTask = queryTimed("
|
<cfset qTask = queryTimed("
|
||||||
SELECT t.ID, t.ClaimedByUserID, t.CompletedOn, t.OrderID, t.TaskTypeID, t.BusinessID,
|
SELECT t.ID, t.ClaimedByUserID, t.CompletedOn, t.OrderID, t.TaskTypeID, t.BusinessID,
|
||||||
o.UserID, o.ServicePointID
|
o.UserID AS CustomerUserID, o.ServicePointID,
|
||||||
|
tt.Name AS TaskTypeName,
|
||||||
|
b.UserID AS BusinessOwnerUserID
|
||||||
FROM Tasks t
|
FROM Tasks t
|
||||||
LEFT JOIN Orders o ON o.ID = t.OrderID
|
LEFT JOIN Orders o ON o.ID = t.OrderID
|
||||||
|
LEFT JOIN tt_TaskTypes tt ON tt.ID = t.TaskTypeID
|
||||||
|
LEFT JOIN Businesses b ON b.ID = t.BusinessID
|
||||||
WHERE t.ID = ?
|
WHERE t.ID = ?
|
||||||
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
|
@ -58,6 +63,7 @@
|
||||||
|
|
||||||
<!--- Chat tasks (TaskTypeID = 2) can be closed without being claimed first --->
|
<!--- Chat tasks (TaskTypeID = 2) can be closed without being claimed first --->
|
||||||
<cfset isChatTask = (qTask.TaskTypeID EQ 2)>
|
<cfset isChatTask = (qTask.TaskTypeID EQ 2)>
|
||||||
|
<cfset isCashTask = (len(trim(qTask.TaskTypeName)) GT 0 AND findNoCase("Cash", qTask.TaskTypeName) GT 0)>
|
||||||
|
|
||||||
<cfif (NOT isChatTask) AND (qTask.ClaimedByUserID EQ 0)>
|
<cfif (NOT isChatTask) AND (qTask.ClaimedByUserID 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." })>
|
||||||
|
|
@ -74,7 +80,8 @@
|
||||||
|
|
||||||
<!--- Check if this is a service point task (customer-facing) --->
|
<!--- Check if this is a service point task (customer-facing) --->
|
||||||
<cfset hasServicePoint = (val(qTask.ServicePointID) GT 0)>
|
<cfset hasServicePoint = (val(qTask.ServicePointID) GT 0)>
|
||||||
<cfset customerUserID = val(qTask.UserID)>
|
<cfset customerUserID = val(qTask.CustomerUserID)>
|
||||||
|
<cfset businessOwnerUserID = val(qTask.BusinessOwnerUserID)>
|
||||||
<cfset workerUserID = val(qTask.ClaimedByUserID)>
|
<cfset workerUserID = val(qTask.ClaimedByUserID)>
|
||||||
<cfset ratingRequired = false>
|
<cfset ratingRequired = false>
|
||||||
<cfset ratingsCreated = []>
|
<cfset ratingsCreated = []>
|
||||||
|
|
@ -101,6 +108,77 @@
|
||||||
</cfif>
|
</cfif>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
|
<!--- === CASH TASK VALIDATION === --->
|
||||||
|
<cfset cashResult = {}>
|
||||||
|
<cfif isCashTask>
|
||||||
|
<cfif CashReceivedCents LTE 0>
|
||||||
|
<cfset apiAbort({ "OK": false, "ERROR": "cash_required", "MESSAGE": "Cash amount is required for cash tasks." })>
|
||||||
|
</cfif>
|
||||||
|
<cfif CashReceivedCents GT 50000>
|
||||||
|
<cfset apiAbort({ "OK": false, "ERROR": "cash_limit", "MESSAGE": "Cash transactions cannot exceed $500." })>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Check 30-day rolling limit for customer --->
|
||||||
|
<cfif customerUserID GT 0>
|
||||||
|
<cfset q30Day = queryTimed("
|
||||||
|
SELECT COALESCE(SUM(PaymentPaidInCash), 0) AS TotalCashDollars
|
||||||
|
FROM Payments
|
||||||
|
WHERE PaymentSentByUserID = ?
|
||||||
|
AND PaymentAddedOn >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||||
|
", [{ value = customerUserID, cfsqltype = "cf_sql_integer" }], { datasource = "payfrit" })>
|
||||||
|
<cfif (val(q30Day.TotalCashDollars) + (CashReceivedCents / 100)) GT 10000>
|
||||||
|
<cfset apiAbort({ "OK": false, "ERROR": "cash_30day_limit", "MESSAGE": "Customer has exceeded the $10,000 rolling 30-day cash limit." })>
|
||||||
|
</cfif>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Calculate order total from line items --->
|
||||||
|
<cfif qTask.OrderID LTE 0>
|
||||||
|
<cfset apiAbort({ "OK": false, "ERROR": "no_order", "MESSAGE": "Cash tasks must be linked to an order." })>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfset qOrderTotal = queryTimed("
|
||||||
|
SELECT SUM(oli.Price * oli.Quantity) AS Subtotal, o.TipAmount, o.DeliveryFee, o.OrderTypeID, b.TaxRate
|
||||||
|
FROM Orders o
|
||||||
|
INNER JOIN Businesses b ON b.ID = o.BusinessID
|
||||||
|
LEFT JOIN OrderLineItems oli ON oli.OrderID = o.ID AND oli.IsDeleted = b'0'
|
||||||
|
WHERE o.ID = ?
|
||||||
|
GROUP BY o.ID
|
||||||
|
", [{ value = qTask.OrderID, cfsqltype = "cf_sql_integer" }], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<cfif qOrderTotal.recordCount EQ 0>
|
||||||
|
<cfset apiAbort({ "OK": false, "ERROR": "no_order", "MESSAGE": "Order not found for this cash task." })>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfset cashSubtotal = val(qOrderTotal.Subtotal)>
|
||||||
|
<cfset cashTax = cashSubtotal * val(qOrderTotal.TaxRate)>
|
||||||
|
<cfset cashTip = val(qOrderTotal.TipAmount)>
|
||||||
|
<cfset cashDeliveryFee = (val(qOrderTotal.OrderTypeID) EQ 3) ? val(qOrderTotal.DeliveryFee) : 0>
|
||||||
|
<cfset cashPlatformFee = cashSubtotal * 0.05>
|
||||||
|
<cfset orderTotalCents = round((cashSubtotal + cashTax + cashTip + cashDeliveryFee + cashPlatformFee) * 100)>
|
||||||
|
|
||||||
|
<cfif CashReceivedCents LT orderTotalCents>
|
||||||
|
<cfset apiAbort({ "OK": false, "ERROR": "insufficient_cash", "MESSAGE": "Cash received ($#numberFormat(CashReceivedCents/100, '0.00')#) is less than order total ($#numberFormat(orderTotalCents/100, '0.00')#)." })>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Calculate cash transaction fee (on order total) --->
|
||||||
|
<cfif orderTotalCents LT 1000>
|
||||||
|
<cfset feeCents = round(orderTotalCents * 0.0225)>
|
||||||
|
<cfelse>
|
||||||
|
<cfset feeCents = 22 + round(orderTotalCents * 0.0005)>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfset changeCents = CashReceivedCents - orderTotalCents>
|
||||||
|
<cfset businessReceivesCents = orderTotalCents - feeCents>
|
||||||
|
|
||||||
|
<cfset cashResult = {
|
||||||
|
"orderTotalCents": orderTotalCents,
|
||||||
|
"cashReceivedCents": CashReceivedCents,
|
||||||
|
"changeCents": changeCents,
|
||||||
|
"feeCents": feeCents,
|
||||||
|
"businessReceivesCents": businessReceivesCents
|
||||||
|
}>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
<!--- Mark task as completed --->
|
<!--- Mark task as completed --->
|
||||||
<cfset queryTimed("
|
<cfset queryTimed("
|
||||||
UPDATE Tasks
|
UPDATE Tasks
|
||||||
|
|
@ -178,6 +256,67 @@
|
||||||
</cfif>
|
</cfif>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
|
<!--- === CASH TRANSACTION PROCESSING === --->
|
||||||
|
<cfset cashProcessed = false>
|
||||||
|
<cfif isCashTask AND CashReceivedCents GT 0>
|
||||||
|
<!--- Credit customer change to their balance --->
|
||||||
|
<cfif changeCents GT 0 AND customerUserID GT 0>
|
||||||
|
<cfset queryTimed("
|
||||||
|
UPDATE Users SET Balance = Balance + :change WHERE ID = :userID
|
||||||
|
", {
|
||||||
|
change: changeCents / 100,
|
||||||
|
userID: customerUserID
|
||||||
|
}, { datasource = "payfrit" })>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Credit Payfrit fee to User 0 (Payfrit Network) --->
|
||||||
|
<cfif feeCents GT 0>
|
||||||
|
<cfset queryTimed("
|
||||||
|
UPDATE Users SET Balance = Balance + :fee WHERE ID = 0
|
||||||
|
", {
|
||||||
|
fee: feeCents / 100
|
||||||
|
}, { datasource = "payfrit" })>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Debit worker (received physical cash, debit digitally) --->
|
||||||
|
<cfif workerUserID_for_payout GT 0>
|
||||||
|
<cfset queryTimed("
|
||||||
|
INSERT INTO WorkPayoutLedgers
|
||||||
|
(UserID, TaskID, GrossEarningsCents, ActivationWithheldCents, NetTransferCents, Status)
|
||||||
|
VALUES
|
||||||
|
(:userID, :taskID, :gross, 0, :net, 'cash_debit')
|
||||||
|
", {
|
||||||
|
userID: workerUserID_for_payout,
|
||||||
|
taskID: TaskID,
|
||||||
|
gross: -CashReceivedCents,
|
||||||
|
net: -CashReceivedCents
|
||||||
|
}, { datasource = "payfrit" })>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Log transaction in Payments table --->
|
||||||
|
<cfset queryTimed("
|
||||||
|
INSERT INTO Payments (
|
||||||
|
PaymentSentByUserID, PaymentReceivedByUserID, PaymentOrderID,
|
||||||
|
PaymentFromCreditCard, PaymentFromPayfritBalance, PaymentPaidInCash,
|
||||||
|
PaymentPayfritsCut, PaymentCreditCardFees, PaymentPayfritNetworkFees,
|
||||||
|
PaymentRemark, PaymentAddedOn
|
||||||
|
) VALUES (
|
||||||
|
:sentBy, :receivedBy, :orderID,
|
||||||
|
0, 0, :cashAmount,
|
||||||
|
:payfritCut, 0, 0,
|
||||||
|
'Cash payment', NOW()
|
||||||
|
)
|
||||||
|
", {
|
||||||
|
sentBy: customerUserID,
|
||||||
|
receivedBy: businessOwnerUserID,
|
||||||
|
orderID: qTask.OrderID,
|
||||||
|
cashAmount: CashReceivedCents / 100,
|
||||||
|
payfritCut: feeCents / 100
|
||||||
|
}, { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<cfset cashProcessed = true>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
<!--- Create rating records for service point tasks --->
|
<!--- Create rating records for service point tasks --->
|
||||||
<cfif hasServicePoint AND customerUserID GT 0 AND workerUserID GT 0>
|
<cfif hasServicePoint AND customerUserID GT 0 AND workerUserID GT 0>
|
||||||
<!--- 1. Customer rates Worker (always created, submitted via receipt link) --->
|
<!--- 1. Customer rates Worker (always created, submitted via receipt link) --->
|
||||||
|
|
@ -225,15 +364,26 @@
|
||||||
</cfif>
|
</cfif>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<cfset apiAbort({
|
<cfset response = {
|
||||||
"OK": true,
|
"OK": true,
|
||||||
"ERROR": "",
|
"ERROR": "",
|
||||||
"MESSAGE": "Task completed successfully.",
|
"MESSAGE": "Task completed successfully.",
|
||||||
"TaskID": TaskID,
|
"TaskID": TaskID,
|
||||||
"OrderUpdated": orderUpdated,
|
"OrderUpdated": orderUpdated,
|
||||||
"RatingsCreated": ratingsCreated,
|
"RatingsCreated": ratingsCreated,
|
||||||
"LedgerCreated": ledgerCreated
|
"LedgerCreated": ledgerCreated,
|
||||||
})>
|
"CashProcessed": cashProcessed
|
||||||
|
}>
|
||||||
|
|
||||||
|
<cfif cashProcessed>
|
||||||
|
<cfset response["CashReceived"] = numberFormat(CashReceivedCents / 100, "0.00")>
|
||||||
|
<cfset response["OrderTotal"] = numberFormat(orderTotalCents / 100, "0.00")>
|
||||||
|
<cfset response["Change"] = numberFormat(changeCents / 100, "0.00")>
|
||||||
|
<cfset response["Fee"] = numberFormat(feeCents / 100, "0.00")>
|
||||||
|
<cfset response["BusinessReceives"] = numberFormat(businessReceivesCents / 100, "0.00")>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfset apiAbort(response)>
|
||||||
|
|
||||||
<cfcatch>
|
<cfcatch>
|
||||||
<cfset apiAbort({
|
<cfset apiAbort({
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
t.ClaimedByUserID,
|
t.ClaimedByUserID,
|
||||||
tc.Name AS CategoryName,
|
tc.Name AS CategoryName,
|
||||||
tc.Color AS CategoryColor,
|
tc.Color AS CategoryColor,
|
||||||
|
tt.Name AS TaskTypeName,
|
||||||
o.ID AS OID,
|
o.ID AS OID,
|
||||||
o.UUID AS OrderUUID,
|
o.UUID AS OrderUUID,
|
||||||
o.UserID,
|
o.UserID,
|
||||||
|
|
@ -54,15 +55,20 @@
|
||||||
o.ServicePointID,
|
o.ServicePointID,
|
||||||
o.Remarks,
|
o.Remarks,
|
||||||
o.SubmittedOn,
|
o.SubmittedOn,
|
||||||
|
o.TipAmount,
|
||||||
|
o.DeliveryFee,
|
||||||
sp.Name AS ServicePointName,
|
sp.Name AS ServicePointName,
|
||||||
sp.TypeID AS ServicePointTypeID,
|
sp.TypeID AS ServicePointTypeID,
|
||||||
|
b.TaxRate,
|
||||||
u.ID AS CustomerUserID,
|
u.ID AS CustomerUserID,
|
||||||
u.FirstName,
|
u.FirstName,
|
||||||
u.LastName,
|
u.LastName,
|
||||||
u.ContactNumber
|
u.ContactNumber
|
||||||
FROM Tasks t
|
FROM Tasks t
|
||||||
LEFT JOIN TaskCategories tc ON tc.ID = t.CategoryID
|
LEFT JOIN TaskCategories tc ON tc.ID = t.CategoryID
|
||||||
|
LEFT JOIN tt_TaskTypes tt ON tt.ID = t.TaskTypeID
|
||||||
LEFT JOIN Orders o ON o.ID = t.OrderID
|
LEFT JOIN Orders o ON o.ID = t.OrderID
|
||||||
|
LEFT JOIN Businesses b ON b.ID = t.BusinessID
|
||||||
LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID
|
LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID
|
||||||
LEFT JOIN Users u ON u.ID = o.UserID
|
LEFT JOIN Users u ON u.ID = o.UserID
|
||||||
WHERE t.ID = ?
|
WHERE t.ID = ?
|
||||||
|
|
@ -99,6 +105,7 @@
|
||||||
"TaskBusinessID": qTask.BusinessID,
|
"TaskBusinessID": qTask.BusinessID,
|
||||||
"TaskCategoryID": qTask.CategoryID,
|
"TaskCategoryID": qTask.CategoryID,
|
||||||
"TaskTypeID": qTask.TaskTypeID ?: 1,
|
"TaskTypeID": qTask.TaskTypeID ?: 1,
|
||||||
|
"TaskTypeName": qTask.TaskTypeName ?: "",
|
||||||
"TaskTitle": taskTitle,
|
"TaskTitle": taskTitle,
|
||||||
"TaskCreatedOn": dateFormat(qTask.CreatedOn, "yyyy-mm-dd") & "T" & timeFormat(qTask.CreatedOn, "HH:mm:ss"),
|
"TaskCreatedOn": dateFormat(qTask.CreatedOn, "yyyy-mm-dd") & "T" & timeFormat(qTask.CreatedOn, "HH:mm:ss"),
|
||||||
"TaskStatusID": qTask.ClaimedByUserID GT 0 ? 1 : 0,
|
"TaskStatusID": qTask.ClaimedByUserID GT 0 ? 1 : 0,
|
||||||
|
|
@ -160,7 +167,9 @@
|
||||||
ORDER BY oli.ID
|
ORDER BY oli.ID
|
||||||
", [ { value = qTask.OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
", [ { value = qTask.OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<cfset subtotal = 0>
|
||||||
<cfloop query="qLineItems">
|
<cfloop query="qLineItems">
|
||||||
|
<cfset subtotal += qLineItems.LineItemPrice * qLineItems.Quantity>
|
||||||
<cfset arrayAppend(result.LineItems, {
|
<cfset arrayAppend(result.LineItems, {
|
||||||
"LineItemID": qLineItems.OrderLineItemID,
|
"LineItemID": qLineItems.OrderLineItemID,
|
||||||
"ParentLineItemID": qLineItems.ParentOrderLineItemID,
|
"ParentLineItemID": qLineItems.ParentOrderLineItemID,
|
||||||
|
|
@ -172,6 +181,14 @@
|
||||||
"IsModifier": qLineItems.ParentOrderLineItemID GT 0
|
"IsModifier": qLineItems.ParentOrderLineItemID GT 0
|
||||||
})>
|
})>
|
||||||
</cfloop>
|
</cfloop>
|
||||||
|
|
||||||
|
<!--- Calculate order total --->
|
||||||
|
<cfset taxRate = val(qTask.TaxRate)>
|
||||||
|
<cfset tax = subtotal * taxRate>
|
||||||
|
<cfset tip = val(qTask.TipAmount)>
|
||||||
|
<cfset deliveryFee = (val(qTask.OrderTypeID) EQ 3) ? val(qTask.DeliveryFee) : 0>
|
||||||
|
<cfset platformFee = subtotal * 0.05>
|
||||||
|
<cfset result["OrderTotal"] = numberFormat(subtotal + tax + tip + deliveryFee + platformFee, "0.00")>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<cfset apiAbort({
|
<cfset apiAbort({
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,28 @@
|
||||||
})>
|
})>
|
||||||
</cfloop>
|
</cfloop>
|
||||||
|
|
||||||
|
<!--- Calculate OrderTotal for tasks with linked orders --->
|
||||||
|
<cfloop from="1" to="#arrayLen(tasks)#" index="i">
|
||||||
|
<cfif val(tasks[i].SourceID) GT 0>
|
||||||
|
<cfset qOT = queryTimed("
|
||||||
|
SELECT SUM(oli.Price * oli.Quantity) AS Subtotal, o.TipAmount, o.DeliveryFee, o.OrderTypeID, b.TaxRate
|
||||||
|
FROM Orders o
|
||||||
|
INNER JOIN Businesses b ON b.ID = o.BusinessID
|
||||||
|
LEFT JOIN OrderLineItems oli ON oli.OrderID = o.ID AND oli.IsDeleted = b'0'
|
||||||
|
WHERE o.ID = ?
|
||||||
|
GROUP BY o.ID
|
||||||
|
", [{ value = tasks[i].SourceID, cfsqltype = "cf_sql_integer" }], { datasource = "payfrit" })>
|
||||||
|
<cfif qOT.recordCount GT 0>
|
||||||
|
<cfset sub = val(qOT.Subtotal)>
|
||||||
|
<cfset tasks[i]["OrderTotal"] = numberFormat(sub + (sub * val(qOT.TaxRate)) + val(qOT.TipAmount) + ((val(qOT.OrderTypeID) EQ 3) ? val(qOT.DeliveryFee) : 0) + (sub * 0.05), "0.00")>
|
||||||
|
<cfelse>
|
||||||
|
<cfset tasks[i]["OrderTotal"] = 0>
|
||||||
|
</cfif>
|
||||||
|
<cfelse>
|
||||||
|
<cfset tasks[i]["OrderTotal"] = 0>
|
||||||
|
</cfif>
|
||||||
|
</cfloop>
|
||||||
|
|
||||||
<cfset apiAbort({
|
<cfset apiAbort({
|
||||||
"OK": true,
|
"OK": true,
|
||||||
"ERROR": "",
|
"ERROR": "",
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,28 @@
|
||||||
})>
|
})>
|
||||||
</cfloop>
|
</cfloop>
|
||||||
|
|
||||||
|
<!--- Calculate OrderTotal for tasks with linked orders --->
|
||||||
|
<cfloop from="1" to="#arrayLen(tasks)#" index="i">
|
||||||
|
<cfif val(tasks[i].SourceID) GT 0>
|
||||||
|
<cfset qOT = queryTimed("
|
||||||
|
SELECT SUM(oli.Price * oli.Quantity) AS Subtotal, o.TipAmount, o.DeliveryFee, o.OrderTypeID, b.TaxRate
|
||||||
|
FROM Orders o
|
||||||
|
INNER JOIN Businesses b ON b.ID = o.BusinessID
|
||||||
|
LEFT JOIN OrderLineItems oli ON oli.OrderID = o.ID AND oli.IsDeleted = b'0'
|
||||||
|
WHERE o.ID = ?
|
||||||
|
GROUP BY o.ID
|
||||||
|
", [{ value = tasks[i].SourceID, cfsqltype = "cf_sql_integer" }], { datasource = "payfrit" })>
|
||||||
|
<cfif qOT.recordCount GT 0>
|
||||||
|
<cfset sub = val(qOT.Subtotal)>
|
||||||
|
<cfset tasks[i]["OrderTotal"] = numberFormat(sub + (sub * val(qOT.TaxRate)) + val(qOT.TipAmount) + ((val(qOT.OrderTypeID) EQ 3) ? val(qOT.DeliveryFee) : 0) + (sub * 0.05), "0.00")>
|
||||||
|
<cfelse>
|
||||||
|
<cfset tasks[i]["OrderTotal"] = 0>
|
||||||
|
</cfif>
|
||||||
|
<cfelse>
|
||||||
|
<cfset tasks[i]["OrderTotal"] = 0>
|
||||||
|
</cfif>
|
||||||
|
</cfloop>
|
||||||
|
|
||||||
<cfset apiAbort({
|
<cfset apiAbort({
|
||||||
"OK": true,
|
"OK": true,
|
||||||
"ERROR": "",
|
"ERROR": "",
|
||||||
|
|
|
||||||
BIN
portal/favicon-512.png
Normal file
BIN
portal/favicon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
|
|
@ -426,7 +426,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SP Sharing: Grants You've Created -->
|
<!-- COMMENTED OUT FOR LAUNCH - Service Point Marketing / Grants (coming soon)
|
||||||
<div class="card" id="spSharingOwnerCard">
|
<div class="card" id="spSharingOwnerCard">
|
||||||
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
|
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
|
||||||
<h3>Grants You've Created</h3>
|
<h3>Grants You've Created</h3>
|
||||||
|
|
@ -436,8 +436,6 @@
|
||||||
<div class="loading-text">Loading...</div>
|
<div class="loading-text">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SP Sharing: Invites & Active Grants -->
|
|
||||||
<div class="card" id="spSharingGuestCard">
|
<div class="card" id="spSharingGuestCard">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Invites & Active Grants</h3>
|
<h3>Invites & Active Grants</h3>
|
||||||
|
|
@ -447,8 +445,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Invite Business Modal -->
|
|
||||||
<div class="modal-overlay" id="inviteBusinessModal" style="display:none">
|
<div class="modal-overlay" id="inviteBusinessModal" style="display:none">
|
||||||
<div class="modal" style="max-width:500px">
|
<div class="modal" style="max-width:500px">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
|
|
@ -502,6 +498,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
END COMMENTED OUT -->
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Services Page (Task Types) -->
|
<!-- Services Page (Task Types) -->
|
||||||
|
|
@ -695,6 +692,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- COMMENTED OUT FOR LAUNCH - Tabs/Running Checks feature (coming soon)
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Tabs / Running Checks</h3>
|
<h3>Tabs / Running Checks</h3>
|
||||||
|
|
@ -725,6 +723,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
END COMMENTED OUT -->
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Menu Builder - Payfrit</title>
|
<title>Menu Builder - Payfrit</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
<link rel="icon" type="image/png" sizes="512x512" href="favicon-512.png">
|
||||||
<link rel="stylesheet" href="portal.css">
|
<link rel="stylesheet" href="portal.css">
|
||||||
<style>
|
<style>
|
||||||
/* Menu Builder Specific Styles */
|
/* Menu Builder Specific Styles */
|
||||||
|
|
@ -992,7 +992,6 @@
|
||||||
<div class="canvas-header">
|
<div class="canvas-header">
|
||||||
<h2>
|
<h2>
|
||||||
<span id="menuName">Menu Builder</span>
|
<span id="menuName">Menu Builder</span>
|
||||||
<span style="font-size: 13px; color: var(--gray-500); font-weight: normal;" id="businessLabel"></span>
|
|
||||||
</h2>
|
</h2>
|
||||||
<div class="canvas-actions" style="display: flex; gap: 8px; align-items: center;">
|
<div class="canvas-actions" style="display: flex; gap: 8px; align-items: center;">
|
||||||
<select id="menuSelector" onchange="MenuBuilder.onMenuSelect(this.value)" style="padding: 8px 12px; border: 1px solid var(--gray-300); border-radius: 6px; background: #fff; font-size: 14px; cursor: pointer;">
|
<select id="menuSelector" onchange="MenuBuilder.onMenuSelect(this.value)" style="padding: 8px 12px; border: 1px solid var(--gray-300); border-radius: 6px; background: #fff; font-size: 14px; cursor: pointer;">
|
||||||
|
|
@ -1200,12 +1199,11 @@
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.OK && data.BUSINESS) {
|
if (data.OK && data.BUSINESS) {
|
||||||
const biz = data.BUSINESS;
|
const biz = data.BUSINESS;
|
||||||
document.getElementById('businessLabel').textContent = `- ${biz.BusinessName}`;
|
|
||||||
// Update sidebar business info
|
// Update sidebar business info
|
||||||
const businessName = document.getElementById('businessName');
|
const businessName = document.getElementById('businessName');
|
||||||
const businessAvatar = document.getElementById('businessAvatar');
|
const businessAvatar = document.getElementById('businessAvatar');
|
||||||
if (businessName) businessName.textContent = biz.BusinessName;
|
if (businessName) businessName.textContent = biz.Name;
|
||||||
if (businessAvatar) businessAvatar.textContent = biz.BusinessName ? biz.BusinessName.charAt(0).toUpperCase() : 'B';
|
if (businessAvatar) businessAvatar.textContent = biz.Name ? biz.Name.charAt(0).toUpperCase() : 'B';
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[MenuBuilder] Error loading business:', err);
|
console.error('[MenuBuilder] Error loading business:', err);
|
||||||
|
|
@ -3288,6 +3286,7 @@
|
||||||
// Store menus list and update selector
|
// Store menus list and update selector
|
||||||
this.menus = data.MENUS || [];
|
this.menus = data.MENUS || [];
|
||||||
this.selectedMenuId = data.SELECTED_MENU_ID || 0;
|
this.selectedMenuId = data.SELECTED_MENU_ID || 0;
|
||||||
|
this.defaultMenuId = data.DEFAULT_MENU_ID || 0;
|
||||||
this.updateMenuSelector();
|
this.updateMenuSelector();
|
||||||
|
|
||||||
// Store templates from API (default to empty array if not provided)
|
// Store templates from API (default to empty array if not provided)
|
||||||
|
|
@ -3305,6 +3304,7 @@
|
||||||
// Still clear the loading message
|
// Still clear the loading message
|
||||||
this.templates = [];
|
this.templates = [];
|
||||||
this.menus = data.MENUS || [];
|
this.menus = data.MENUS || [];
|
||||||
|
this.defaultMenuId = data.DEFAULT_MENU_ID || 0;
|
||||||
this.updateMenuSelector();
|
this.updateMenuSelector();
|
||||||
this.renderTemplateLibrary();
|
this.renderTemplateLibrary();
|
||||||
}
|
}
|
||||||
|
|
@ -3346,14 +3346,18 @@
|
||||||
<div id="menuManagerList" style="display: flex; flex-direction: column; gap: 8px;">
|
<div id="menuManagerList" style="display: flex; flex-direction: column; gap: 8px;">
|
||||||
${this.menus.length === 0 ? '<p style="color: var(--gray-500); text-align: center;">No menus created yet. Click "Add Menu" to create one.</p>' : ''}
|
${this.menus.length === 0 ? '<p style="color: var(--gray-500); text-align: center;">No menus created yet. Click "Add Menu" to create one.</p>' : ''}
|
||||||
${this.menus.map(menu => `
|
${this.menus.map(menu => `
|
||||||
<div style="display: flex; align-items: center; gap: 12px; padding: 12px; background: var(--gray-50); border-radius: 8px;">
|
<div style="display: flex; align-items: center; gap: 12px; padding: 12px; background: var(--gray-50); border-radius: 8px; ${menu.MenuID === this.defaultMenuId ? 'border: 2px solid var(--primary-color);' : ''}">
|
||||||
<div style="flex: 1;">
|
<div style="flex: 1;">
|
||||||
<div style="font-weight: 600;">${this.escapeHtml(menu.MenuName)}</div>
|
<div style="font-weight: 600;">
|
||||||
|
${this.escapeHtml(menu.MenuName)}
|
||||||
|
${menu.MenuID === this.defaultMenuId ? '<span style="color: var(--primary-color); font-size: 12px; margin-left: 8px;">★ Default</span>' : ''}
|
||||||
|
</div>
|
||||||
<div style="font-size: 12px; color: var(--gray-500);">
|
<div style="font-size: 12px; color: var(--gray-500);">
|
||||||
${menu.MenuStartTime && menu.MenuEndTime ? `${menu.MenuStartTime} - ${menu.MenuEndTime}` : 'All day'}
|
${menu.MenuStartTime && menu.MenuEndTime ? `${menu.MenuStartTime} - ${menu.MenuEndTime}` : 'All day'}
|
||||||
· ${this.formatDaysActive(menu.MenuDaysActive)}
|
· ${this.formatDaysActive(menu.MenuDaysActive)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${menu.MenuID !== this.defaultMenuId ? `<button class="btn btn-sm" onclick="MenuBuilder.setDefaultMenu(${menu.MenuID})" title="Set as default menu">Set Default</button>` : ''}
|
||||||
<button class="btn btn-sm btn-secondary" onclick="MenuBuilder.editMenu(${menu.MenuID})">Edit</button>
|
<button class="btn btn-sm btn-secondary" onclick="MenuBuilder.editMenu(${menu.MenuID})">Edit</button>
|
||||||
<button class="btn btn-sm btn-danger" onclick="MenuBuilder.deleteMenu(${menu.MenuID})">Delete</button>
|
<button class="btn btn-sm btn-danger" onclick="MenuBuilder.deleteMenu(${menu.MenuID})">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3530,6 +3534,31 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async setDefaultMenu(menuId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/menu/menus.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
BusinessID: this.config.businessId,
|
||||||
|
action: 'setDefault',
|
||||||
|
MenuID: menuId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.OK) {
|
||||||
|
this.defaultMenuId = menuId;
|
||||||
|
this.toast('Default menu updated', 'success');
|
||||||
|
// Refresh the menu manager display
|
||||||
|
this.showMenuManager();
|
||||||
|
} else {
|
||||||
|
this.toast(data.MESSAGE || 'Failed to set default menu', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.toast('Error setting default menu', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Render template library in sidebar
|
// Render template library in sidebar
|
||||||
renderTemplateLibrary() {
|
renderTemplateLibrary() {
|
||||||
const container = document.getElementById('templateLibrary');
|
const container = document.getElementById('templateLibrary');
|
||||||
|
|
|
||||||
308
portal/portal.js
308
portal/portal.js
|
|
@ -754,60 +754,52 @@ const Portal = {
|
||||||
// Render hours editor
|
// Render hours editor
|
||||||
this.renderHoursEditor(biz.BUSINESSHOURSDETAIL || biz.HoursDetail || []);
|
this.renderHoursEditor(biz.BUSINESSHOURSDETAIL || biz.HoursDetail || []);
|
||||||
|
|
||||||
// Load tab settings
|
// COMMENTED OUT FOR LAUNCH - Tabs/Running Checks (coming soon)
|
||||||
const tabsEnabled = biz.SESSIONENABLED || biz.SessionEnabled || 0;
|
// const tabsEnabled = biz.SESSIONENABLED || biz.SessionEnabled || 0;
|
||||||
const tabLockMinutes = biz.SESSIONLOCKMINUTES || biz.SessionLockMinutes || 30;
|
// const tabLockMinutes = biz.SESSIONLOCKMINUTES || biz.SessionLockMinutes || 30;
|
||||||
const tabPaymentStrategy = biz.SESSIONPAYMENTSTRATEGY || biz.SessionPaymentStrategy || 'A';
|
// const tabPaymentStrategy = biz.SESSIONPAYMENTSTRATEGY || biz.SessionPaymentStrategy || 'A';
|
||||||
|
// const tabsCheckbox = document.getElementById('tabsEnabled');
|
||||||
const tabsCheckbox = document.getElementById('tabsEnabled');
|
// const tabDetails = document.getElementById('tabSettingsDetails');
|
||||||
const tabDetails = document.getElementById('tabSettingsDetails');
|
// if (tabsCheckbox) {
|
||||||
if (tabsCheckbox) {
|
// tabsCheckbox.checked = tabsEnabled == 1;
|
||||||
tabsCheckbox.checked = tabsEnabled == 1;
|
// if (tabDetails) tabDetails.style.display = tabsEnabled == 1 ? 'block' : 'none';
|
||||||
if (tabDetails) tabDetails.style.display = tabsEnabled == 1 ? 'block' : 'none';
|
// }
|
||||||
}
|
// const lockInput = document.getElementById('tabLockMinutes');
|
||||||
const lockInput = document.getElementById('tabLockMinutes');
|
// if (lockInput) lockInput.value = tabLockMinutes;
|
||||||
if (lockInput) lockInput.value = tabLockMinutes;
|
// const strategySelect = document.getElementById('tabPaymentStrategy');
|
||||||
const strategySelect = document.getElementById('tabPaymentStrategy');
|
// if (strategySelect) strategySelect.value = tabPaymentStrategy;
|
||||||
if (strategySelect) strategySelect.value = tabPaymentStrategy;
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Portal] Error loading business info:', err);
|
console.error('[Portal] Error loading business info:', err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Save tab settings
|
// COMMENTED OUT FOR LAUNCH - Tabs/Running Checks (coming soon)
|
||||||
async saveTabSettings() {
|
// async saveTabSettings() {
|
||||||
const tabsEnabled = document.getElementById('tabsEnabled').checked ? 1 : 0;
|
// const tabsEnabled = document.getElementById('tabsEnabled').checked ? 1 : 0;
|
||||||
const tabLockMinutes = parseInt(document.getElementById('tabLockMinutes').value) || 30;
|
// const tabLockMinutes = parseInt(document.getElementById('tabLockMinutes').value) || 30;
|
||||||
const tabPaymentStrategy = document.getElementById('tabPaymentStrategy').value || 'A';
|
// const tabPaymentStrategy = document.getElementById('tabPaymentStrategy').value || 'A';
|
||||||
|
// const tabDetails = document.getElementById('tabSettingsDetails');
|
||||||
// Show/hide details based on toggle
|
// if (tabDetails) tabDetails.style.display = tabsEnabled ? 'block' : 'none';
|
||||||
const tabDetails = document.getElementById('tabSettingsDetails');
|
// try {
|
||||||
if (tabDetails) tabDetails.style.display = tabsEnabled ? 'block' : 'none';
|
// const response = await fetch(`${this.config.apiBaseUrl}/businesses/updateTabs.cfm`, {
|
||||||
|
// method: 'POST',
|
||||||
try {
|
// headers: { 'Content-Type': 'application/json' },
|
||||||
const response = await fetch(`${this.config.apiBaseUrl}/businesses/updateTabs.cfm`, {
|
// body: JSON.stringify({
|
||||||
method: 'POST',
|
// BusinessID: this.config.businessId,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// SessionEnabled: tabsEnabled,
|
||||||
body: JSON.stringify({
|
// SessionLockMinutes: tabLockMinutes,
|
||||||
BusinessID: this.config.businessId,
|
// SessionPaymentStrategy: tabPaymentStrategy
|
||||||
SessionEnabled: tabsEnabled,
|
// })
|
||||||
SessionLockMinutes: tabLockMinutes,
|
// });
|
||||||
SessionPaymentStrategy: tabPaymentStrategy
|
// const data = await response.json();
|
||||||
})
|
// if (data.OK) { this.showToast('Tab settings saved!', 'success'); }
|
||||||
});
|
// else { this.showToast(data.ERROR || 'Failed to save tab settings', 'error'); }
|
||||||
const data = await response.json();
|
// } catch (err) {
|
||||||
|
// console.error('[Portal] Error saving tab settings:', err);
|
||||||
if (data.OK) {
|
// this.showToast('Error saving tab settings', 'error');
|
||||||
this.showToast('Tab settings saved!', 'success');
|
// }
|
||||||
} else {
|
// },
|
||||||
this.showToast(data.ERROR || 'Failed to save tab settings', 'error');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Portal] Error saving tab settings:', err);
|
|
||||||
this.showToast('Error saving tab settings', 'error');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Render hours editor
|
// Render hours editor
|
||||||
renderHoursEditor(hours) {
|
renderHoursEditor(hours) {
|
||||||
|
|
@ -1590,8 +1582,9 @@ const Portal = {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.loadServicePoints(),
|
this.loadServicePoints(),
|
||||||
this.loadBeacons(),
|
this.loadBeacons(),
|
||||||
this.loadAssignments(),
|
this.loadAssignments()
|
||||||
this.loadSPSharingPage()
|
// COMMENTED OUT FOR LAUNCH - SP Marketing (coming soon)
|
||||||
|
// this.loadSPSharingPage()
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -3840,209 +3833,18 @@ const Portal = {
|
||||||
_spSharingSelectedBizID: 0,
|
_spSharingSelectedBizID: 0,
|
||||||
_spSharingSearchTimer: null,
|
_spSharingSearchTimer: null,
|
||||||
|
|
||||||
async loadSPSharingPage() {
|
/* COMMENTED OUT FOR LAUNCH - SP Marketing / Grants (coming soon)
|
||||||
const bizId = this.config.businessId;
|
async loadSPSharingPage() { ... },
|
||||||
if (!bizId) return;
|
showInviteBusinessModal() { ... },
|
||||||
|
closeInviteModal() { ... },
|
||||||
const STATUS_LABELS = { 0: 'Pending', 1: 'Active', 2: 'Declined', 3: 'Revoked' };
|
toggleEconValue() { ... },
|
||||||
const STATUS_COLORS = { 0: '#f59e0b', 1: '#10b981', 2: '#ef4444', 3: '#ef4444' };
|
async searchBusinessForInvite() { ... },
|
||||||
|
selectInviteBiz(bizID, name) { ... },
|
||||||
// Load owner grants
|
async submitGrantInvite() { ... },
|
||||||
try {
|
async revokeGrant(grantID) { ... },
|
||||||
const ownerData = await this.api('/api/grants/list.cfm', { BusinessID: bizId, Role: 'owner' });
|
async acceptGrant(grantID) { ... },
|
||||||
const ownerEl = document.getElementById('ownerGrantsList');
|
async declineGrant(grantID) { ... }
|
||||||
if (ownerData.OK && ownerData.Grants.length) {
|
*/
|
||||||
let html = '<table class="data-table"><thead><tr><th>Guest Business</th><th>Service Point</th><th>Economics</th><th>Status</th><th>Actions</th></tr></thead><tbody>';
|
|
||||||
ownerData.Grants.forEach(g => {
|
|
||||||
let econ = g.EconomicsType === 'flat_fee' ? `$${parseFloat(g.EconomicsValue).toFixed(2)}/order` : g.EconomicsType === 'percent_of_orders' ? `${parseFloat(g.EconomicsValue).toFixed(1)}%` : 'None';
|
|
||||||
let actions = '';
|
|
||||||
if (g.StatusID === 0 || g.StatusID === 1) {
|
|
||||||
actions = `<button class="btn btn-sm btn-danger" onclick="Portal.revokeGrant(${g.GrantID})">Revoke</button>`;
|
|
||||||
}
|
|
||||||
html += `<tr><td>${this.escapeHtml(g.GuestBusinessName)} (#${g.GuestBusinessID})</td><td>${this.escapeHtml(g.ServicePointName)}</td><td>${econ}</td><td><span style="color:${STATUS_COLORS[g.StatusID]}">${STATUS_LABELS[g.StatusID]}</span></td><td>${actions}</td></tr>`;
|
|
||||||
});
|
|
||||||
html += '</tbody></table>';
|
|
||||||
ownerEl.innerHTML = html;
|
|
||||||
} else {
|
|
||||||
ownerEl.innerHTML = '<p style="color:#999;text-align:center;padding:20px">No grants created yet. Use "Invite Business" to share your service points.</p>';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Portal] Error loading owner grants:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load guest grants
|
|
||||||
try {
|
|
||||||
const guestData = await this.api('/api/grants/list.cfm', { BusinessID: bizId, Role: 'guest' });
|
|
||||||
const guestEl = document.getElementById('guestGrantsList');
|
|
||||||
if (guestData.OK && guestData.Grants.length) {
|
|
||||||
let html = '<table class="data-table"><thead><tr><th>Owner Business</th><th>Service Point</th><th>Economics</th><th>Eligibility</th><th>Status</th><th>Actions</th></tr></thead><tbody>';
|
|
||||||
guestData.Grants.forEach(g => {
|
|
||||||
let econ = g.EconomicsType === 'flat_fee' ? `$${parseFloat(g.EconomicsValue).toFixed(2)}/order` : g.EconomicsType === 'percent_of_orders' ? `${parseFloat(g.EconomicsValue).toFixed(1)}%` : 'None';
|
|
||||||
let actions = '';
|
|
||||||
if (g.StatusID === 0) {
|
|
||||||
actions = `<button class="btn btn-sm btn-primary" onclick="Portal.acceptGrant(${g.GrantID})">Accept</button> <button class="btn btn-sm" onclick="Portal.declineGrant(${g.GrantID})">Decline</button>`;
|
|
||||||
}
|
|
||||||
html += `<tr><td>${this.escapeHtml(g.OwnerBusinessName)} (#${g.OwnerBusinessID})</td><td>${this.escapeHtml(g.ServicePointName)}</td><td>${econ}</td><td>${g.EligibilityScope}</td><td><span style="color:${STATUS_COLORS[g.StatusID]}">${STATUS_LABELS[g.StatusID]}</span></td><td>${actions}</td></tr>`;
|
|
||||||
});
|
|
||||||
html += '</tbody></table>';
|
|
||||||
guestEl.innerHTML = html;
|
|
||||||
} else {
|
|
||||||
guestEl.innerHTML = '<p style="color:#999;text-align:center;padding:20px">No invites or active grants from other businesses.</p>';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Portal] Error loading guest grants:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-load service points for the invite modal
|
|
||||||
try {
|
|
||||||
const spData = await this.api('/api/servicepoints/list.cfm', { BusinessID: bizId });
|
|
||||||
const select = document.getElementById('inviteSPSelect');
|
|
||||||
if (spData.OK && spData.SERVICEPOINTS) {
|
|
||||||
select.innerHTML = spData.SERVICEPOINTS.map(sp => `<option value="${sp.ServicePointID}">${this.escapeHtml(sp.Name)}</option>`).join('');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Portal] Error loading service points:', err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showInviteBusinessModal() {
|
|
||||||
this._spSharingSelectedBizID = 0;
|
|
||||||
document.getElementById('inviteBizSearch').value = '';
|
|
||||||
document.getElementById('inviteBizResults').innerHTML = '';
|
|
||||||
document.getElementById('inviteBizSelected').style.display = 'none';
|
|
||||||
document.getElementById('inviteEconType').value = 'none';
|
|
||||||
document.getElementById('inviteEconValue').style.display = 'none';
|
|
||||||
document.getElementById('inviteEconValue').value = '';
|
|
||||||
document.getElementById('inviteEligibility').value = 'public';
|
|
||||||
document.getElementById('inviteTimePolicy').value = 'always';
|
|
||||||
document.getElementById('inviteBusinessModal').style.display = 'flex';
|
|
||||||
},
|
|
||||||
|
|
||||||
closeInviteModal() {
|
|
||||||
document.getElementById('inviteBusinessModal').style.display = 'none';
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleEconValue() {
|
|
||||||
const type = document.getElementById('inviteEconType').value;
|
|
||||||
const valEl = document.getElementById('inviteEconValue');
|
|
||||||
valEl.style.display = (type === 'none') ? 'none' : 'block';
|
|
||||||
if (type === 'flat_fee') valEl.placeholder = 'Dollar amount per order';
|
|
||||||
else if (type === 'percent_of_orders') valEl.placeholder = 'Percentage (e.g., 10)';
|
|
||||||
},
|
|
||||||
|
|
||||||
async searchBusinessForInvite() {
|
|
||||||
clearTimeout(this._spSharingSearchTimer);
|
|
||||||
const query = document.getElementById('inviteBizSearch').value.trim();
|
|
||||||
if (query.length < 2) {
|
|
||||||
document.getElementById('inviteBizResults').innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._spSharingSearchTimer = setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
const data = await this.api('/api/grants/searchBusiness.cfm', {
|
|
||||||
Query: query,
|
|
||||||
ExcludeBusinessID: this.config.businessId
|
|
||||||
});
|
|
||||||
const resultsEl = document.getElementById('inviteBizResults');
|
|
||||||
if (data.OK && data.Businesses.length) {
|
|
||||||
resultsEl.innerHTML = data.Businesses.map(b =>
|
|
||||||
`<div style="padding:6px 8px;cursor:pointer;border-radius:4px;margin-bottom:2px" class="search-result-item" onmouseover="this.style.background='var(--bg-hover,#f0f0f0)'" onmouseout="this.style.background=''" onclick="Portal.selectInviteBiz(${b.BusinessID},'${this.escapeHtml(b.Name).replace(/'/g, "\\'")}')">${this.escapeHtml(b.Name)} (#${b.BusinessID})</div>`
|
|
||||||
).join('');
|
|
||||||
} else {
|
|
||||||
resultsEl.innerHTML = '<div style="padding:6px;color:#999">No businesses found</div>';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Portal] Business search error:', err);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
},
|
|
||||||
|
|
||||||
selectInviteBiz(bizID, name) {
|
|
||||||
this._spSharingSelectedBizID = bizID;
|
|
||||||
document.getElementById('inviteBizResults').innerHTML = '';
|
|
||||||
document.getElementById('inviteBizSearch').value = '';
|
|
||||||
document.getElementById('inviteBizSelected').style.display = 'block';
|
|
||||||
document.getElementById('inviteBizSelectedName').textContent = `${name} (#${bizID})`;
|
|
||||||
},
|
|
||||||
|
|
||||||
async submitGrantInvite() {
|
|
||||||
if (!this._spSharingSelectedBizID) {
|
|
||||||
this.toast('Please select a business to invite', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const spID = parseInt(document.getElementById('inviteSPSelect').value);
|
|
||||||
if (!spID) {
|
|
||||||
this.toast('Please select a service point', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
OwnerBusinessID: this.config.businessId,
|
|
||||||
GuestBusinessID: this._spSharingSelectedBizID,
|
|
||||||
ServicePointID: spID,
|
|
||||||
EconomicsType: document.getElementById('inviteEconType').value,
|
|
||||||
EconomicsValue: parseFloat(document.getElementById('inviteEconValue').value) || 0,
|
|
||||||
EligibilityScope: document.getElementById('inviteEligibility').value,
|
|
||||||
TimePolicyType: document.getElementById('inviteTimePolicy').value
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await this.api('/api/grants/create.cfm', payload);
|
|
||||||
if (data.OK) {
|
|
||||||
this.toast('Invite sent successfully', 'success');
|
|
||||||
this.closeInviteModal();
|
|
||||||
await this.loadSPSharingPage();
|
|
||||||
} else {
|
|
||||||
this.toast(data.MESSAGE || data.ERROR || 'Error creating grant', 'error');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.toast('Error sending invite', 'error');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async revokeGrant(grantID) {
|
|
||||||
if (!confirm('Revoke this grant? All access will stop immediately.')) return;
|
|
||||||
try {
|
|
||||||
const data = await this.api('/api/grants/revoke.cfm', { GrantID: grantID });
|
|
||||||
if (data.OK) {
|
|
||||||
this.toast('Grant revoked', 'success');
|
|
||||||
await this.loadSPSharingPage();
|
|
||||||
} else {
|
|
||||||
this.toast(data.MESSAGE || 'Error revoking grant', 'error');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.toast('Error revoking grant', 'error');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async acceptGrant(grantID) {
|
|
||||||
try {
|
|
||||||
const data = await this.api('/api/grants/accept.cfm', { GrantID: grantID });
|
|
||||||
if (data.OK) {
|
|
||||||
this.toast('Grant accepted! Service point access is now active.', 'success');
|
|
||||||
await this.loadSPSharingPage();
|
|
||||||
} else {
|
|
||||||
this.toast(data.MESSAGE || 'Error accepting grant', 'error');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.toast('Error accepting grant', 'error');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async declineGrant(grantID) {
|
|
||||||
if (!confirm('Decline this invite?')) return;
|
|
||||||
try {
|
|
||||||
const data = await this.api('/api/grants/decline.cfm', { GrantID: grantID });
|
|
||||||
if (data.OK) {
|
|
||||||
this.toast('Grant declined', 'success');
|
|
||||||
await this.loadSPSharingPage();
|
|
||||||
} else {
|
|
||||||
this.toast(data.MESSAGE || 'Error declining grant', 'error');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.toast('Error declining grant', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize on load
|
// Initialize on load
|
||||||
|
|
|
||||||
Reference in a new issue