Compare commits

..

No commits in common. "0d04ae8463e88c0d2bc84842e9403b6bf763a668" and "a2be30b952ee0da933b67efc86b18d7918522407" have entirely different histories.

19 changed files with 5328 additions and 4675 deletions

View file

@ -108,15 +108,12 @@ try {
} }
try{logPerf(0);}catch(any e){} try{logPerf(0);}catch(any e){}
resp = { writeOutput(serializeJSON({
"OK": true, "OK": true,
"UUID": userUUID, "UUID": userUUID,
"MESSAGE": smsMessage "MESSAGE": smsMessage,
}; "DEV_OTP": otp
if (isDev) { }));
resp["DEV_OTP"] = otp;
}
writeOutput(serializeJSON(resp));
} catch (any e) { } catch (any e) {
apiAbort({ apiAbort({

View file

@ -150,15 +150,12 @@ try {
} }
} }
resp = { writeOutput(serializeJSON({
"OK": true, "OK": true,
"UUID": userUUID, "UUID": userUUID,
"MESSAGE": smsMessage "MESSAGE": smsMessage,
}; "DEV_OTP": otp
if (isDev) { }));
resp["DEV_OTP"] = otp;
}
writeOutput(serializeJSON(resp));
} catch (any e) { } catch (any e) {
apiAbort({ apiAbort({

View file

@ -2,9 +2,8 @@
// Save menu data from the builder UI (OPTIMIZED) // Save menu data from the builder UI (OPTIMIZED)
// Input: BusinessID, Menu (JSON structure) // Input: BusinessID, Menu (JSON structure)
// Output: { OK: true } // Output: { OK: true }
// VERSION: 2026-02-08-fix1
response = { "OK": false, "VERSION": "2026-02-08-fix2", "DEBUG": [] }; response = { "OK": false };
// Track which templates we've already saved options for (to avoid duplicate saves) // Track which templates we've already saved options for (to avoid duplicate saves)
savedTemplates = {}; savedTemplates = {};
@ -96,21 +95,18 @@ try {
throw("Menu categories are required"); throw("Menu categories are required");
} }
// Check if Categories table has data (must match getForBuilder logic!) // Check if new schema is active
// If Categories table has data, use legacy schema (CategoryID in Items) newSchemaActive = false;
// Otherwise use unified schema (ParentItemID in Items)
hasCategoriesData = false;
try { try {
qCatCheck = queryTimed(" qCheck = queryTimed("
SELECT 1 FROM Categories SELECT 1 FROM Items
WHERE BusinessID = :businessID WHERE BusinessID = :businessID AND BusinessID > 0
LIMIT 1 LIMIT 1
", { businessID: businessID }, { datasource: "payfrit" }); ", { businessID: businessID });
hasCategoriesData = (qCatCheck.recordCount > 0); newSchemaActive = (qCheck.recordCount > 0);
} catch (any e) { } catch (any e) {
hasCategoriesData = false; newSchemaActive = false;
} }
newSchemaActive = !hasCategoriesData;
// Wrap everything in a transaction for speed and consistency // Wrap everything in a transaction for speed and consistency
transaction { transaction {
@ -118,13 +114,6 @@ try {
for (cat in menu.categories) { for (cat in menu.categories) {
categoryID = 0; categoryID = 0;
categoryDbId = structKeyExists(cat, "dbId") ? val(cat.dbId) : 0; categoryDbId = structKeyExists(cat, "dbId") ? val(cat.dbId) : 0;
catItemCount = structKeyExists(cat, "items") && isArray(cat.items) ? arrayLen(cat.items) : 0;
// Debug: log each category being processed
arrayAppend(response.DEBUG, "Category: " & cat.name & " (dbId=" & categoryDbId & ") with " & catItemCount & " items");
// Initialize menuId param - use 0 for "no menu" (nullable in DB)
categoryMenuId = structKeyExists(cat, "menuId") ? val(cat.menuId) : 0;
if (newSchemaActive) { if (newSchemaActive) {
if (categoryDbId > 0) { if (categoryDbId > 0) {
@ -162,27 +151,31 @@ try {
categoryID = result.newID; categoryID = result.newID;
} }
} else { } else {
// Get menu ID from category if provided
categoryMenuId = structKeyExists(cat, "menuId") ? val(cat.menuId) : 0;
categoryMenuIdParam = categoryMenuId > 0 ? categoryMenuId : javaCast("null", "");
if (categoryDbId > 0) { if (categoryDbId > 0) {
categoryID = categoryDbId; categoryID = categoryDbId;
queryTimed(" queryTimed("
UPDATE Categories UPDATE Categories
SET Name = :name, SET Name = :name,
SortOrder = :sortOrder, SortOrder = :sortOrder,
MenuID = NULLIF(:menuId, 0) MenuID = :menuId
WHERE ID = :categoryID WHERE CategoryID = :categoryID
", { ", {
categoryID: categoryID, categoryID: categoryID,
name: cat.name, name: cat.name,
sortOrder: catSortOrder, sortOrder: catSortOrder,
menuId: categoryMenuId menuId: categoryMenuIdParam
}); });
} else { } else {
queryTimed(" queryTimed("
INSERT INTO Categories (BusinessID, MenuID, Name, SortOrder, AddedOn) INSERT INTO Categories (BusinessID, MenuID, Name, SortOrder, AddedOn)
VALUES (:businessID, NULLIF(:menuId, 0), :name, :sortOrder, NOW()) VALUES (:businessID, :menuId, :name, :sortOrder, NOW())
", { ", {
businessID: businessID, businessID: businessID,
menuId: categoryMenuId, menuId: categoryMenuIdParam,
name: cat.name, name: cat.name,
sortOrder: catSortOrder sortOrder: catSortOrder
}); });
@ -192,9 +185,6 @@ try {
} }
} }
// Debug: log final categoryID for this category
arrayAppend(response.DEBUG, " -> CategoryID resolved to: " & categoryID);
// Process items // Process items
if (structKeyExists(cat, "items") && isArray(cat.items)) { if (structKeyExists(cat, "items") && isArray(cat.items)) {
itemSortOrder = 0; itemSortOrder = 0;
@ -202,9 +192,6 @@ try {
itemID = 0; itemID = 0;
itemDbId = structKeyExists(item, "dbId") ? val(item.dbId) : 0; itemDbId = structKeyExists(item, "dbId") ? val(item.dbId) : 0;
// Debug: log each item being processed
arrayAppend(response.DEBUG, " Item: " & item.name & " (dbId=" & itemDbId & ") -> CategoryID=" & categoryID);
if (itemDbId > 0) { if (itemDbId > 0) {
itemID = itemDbId; itemID = itemDbId;

View file

@ -4,13 +4,6 @@
<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">
<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>
<cftry> <cftry>
<cfset headersDir = expandPath("/uploads/headers")> <cfset headersDir = expandPath("/uploads/headers")>
<cfscript> <cfscript>
@ -42,29 +35,20 @@ if (bizId LTE 0) {
<cffile action="UPLOAD" filefield="header" destination="#headersDir#/" nameconflict="MAKEUNIQUE" mode="755" result="uploadResult"> <cffile action="UPLOAD" filefield="header" destination="#headersDir#/" nameconflict="MAKEUNIQUE" mode="755" result="uploadResult">
<!--- Get image info and detect actual format ---> <!--- Get image info and detect actual format --->
<cfset actualExt = ""> <cfimage source="#headersDir#/#uploadResult.ServerFile#" action="info" structName="imageInfo">
<cftry>
<cfimage source="#headersDir#/#uploadResult.ServerFile#" action="info" structName="imageInfo">
<!--- Use the actual detected format from source_file if available --->
<cfif structKeyExists(imageInfo, "source_file")>
<cfset actualFormat = lCase(imageInfo.source_file)>
<cfif findNoCase("jpeg", actualFormat) OR findNoCase("jpg", actualFormat)>
<cfset actualExt = "jpg">
<cfelseif findNoCase("png", actualFormat)>
<cfset actualExt = "png">
<cfelseif findNoCase("gif", actualFormat)>
<cfset actualExt = "gif">
<cfelseif findNoCase("webp", actualFormat)>
<cfset actualExt = "webp">
</cfif>
</cfif>
<cfcatch>
<!--- cfimage failed - will use fallback detection below --->
</cfcatch>
</cftry>
<!--- Fallback: if cfimage didn't give us format, detect from file ---> <!--- Use the actual detected format, not the client-provided extension --->
<cfif NOT len(actualExt)> <cfset actualFormat = lCase(imageInfo.source_file)>
<cfset actualExt = "">
<cfif findNoCase("jpeg", actualFormat) OR findNoCase("jpg", actualFormat)>
<cfset actualExt = "jpg">
<cfelseif findNoCase("png", actualFormat)>
<cfset actualExt = "png">
<cfelseif findNoCase("gif", actualFormat)>
<cfset actualExt = "gif">
<cfelseif findNoCase("webp", actualFormat)>
<cfset actualExt = "webp">
<cfelse>
<!--- Fallback: detect by reading first bytes (magic numbers) ---> <!--- Fallback: detect by reading first bytes (magic numbers) --->
<cffile action="readbinary" file="#headersDir#/#uploadResult.ServerFile#" variable="fileBytes"> <cffile action="readbinary" file="#headersDir#/#uploadResult.ServerFile#" variable="fileBytes">
<cfset firstBytes = left(binaryEncode(fileBytes, "hex"), 16)> <cfset firstBytes = left(binaryEncode(fileBytes, "hex"), 16)>
@ -121,15 +105,8 @@ if (bizId LTE 0) {
</cftry> </cftry>
</cfif> </cfif>
<!--- Verify uploaded file still exists before rename --->
<cfset sourceFile = "#headersDir#/#uploadResult.ServerFile#">
<cfif NOT fileExists(sourceFile)>
<cfoutput>#serializeJSON({ "OK": false, "ERROR": "upload_failed", "MESSAGE": "Uploaded file not found - upload may have failed", "DEBUG_SOURCE": sourceFile })#</cfoutput>
<cfabort>
</cfif>
<!--- Rename to BusinessID.ext ---> <!--- Rename to BusinessID.ext --->
<cffile action="RENAME" source="#sourceFile#" destination="#destFile#" mode="755"> <cffile action="RENAME" source="#headersDir#/#uploadResult.ServerFile#" destination="#destFile#" mode="755">
<!--- Update database ---> <!--- Update database --->
<cfquery datasource="payfrit"> <cfquery datasource="payfrit">
@ -139,15 +116,13 @@ if (bizId LTE 0) {
</cfquery> </cfquery>
<!--- Return success with image URL ---> <!--- Return success with image URL --->
<cfset imgWidth = isDefined("imageInfo.width") ? imageInfo.width : 0>
<cfset imgHeight = isDefined("imageInfo.height") ? imageInfo.height : 0>
<cfoutput>#serializeJSON({ <cfoutput>#serializeJSON({
"OK": true, "OK": true,
"ERROR": "", "ERROR": "",
"MESSAGE": "Header uploaded successfully", "MESSAGE": "Header uploaded successfully",
"HEADERURL": "/uploads/headers/#bizId#.#actualExt#", "HEADERURL": "/uploads/headers/#bizId#.#actualExt#",
"WIDTH": imgWidth, "WIDTH": imageInfo.width,
"HEIGHT": imgHeight "HEIGHT": imageInfo.height
})#</cfoutput> })#</cfoutput>
<cfcatch type="any"> <cfcatch type="any">

View file

@ -122,22 +122,16 @@ img = fixOrientation(img);
// Create thumbnail (64x64 square for list view - 2x for retina) // Create thumbnail (64x64 square for list view - 2x for retina)
thumb = createSquareThumb(img, 128); thumb = createSquareThumb(img, 128);
thumbPath = "#itemsDir#/#itemId#_thumb.jpg"; imageWrite(thumb, "#itemsDir#/#itemId#_thumb.jpg", 0.85);
imageWrite(thumb, thumbPath, 0.85);
fileSetAccessMode(thumbPath, "644");
// Create medium size (400px max for detail view) // Create medium size (400px max for detail view)
medium = imageCopy(img, 0, 0, imageGetWidth(img), imageGetHeight(img)); medium = imageCopy(img, 0, 0, imageGetWidth(img), imageGetHeight(img));
medium = resizeToFit(medium, 400); medium = resizeToFit(medium, 400);
mediumPath = "#itemsDir#/#itemId#_medium.jpg"; imageWrite(medium, "#itemsDir#/#itemId#_medium.jpg", 0.85);
imageWrite(medium, mediumPath, 0.85);
fileSetAccessMode(mediumPath, "644");
// Save full size (max 1200px to keep file sizes reasonable) // Save full size (max 1200px to keep file sizes reasonable)
img = resizeToFit(img, 1200); img = resizeToFit(img, 1200);
fullPath = "#itemsDir#/#itemId#.jpg"; imageWrite(img, "#itemsDir#/#itemId#.jpg", 0.90);
imageWrite(img, fullPath, 0.90);
fileSetAccessMode(fullPath, "644");
// Delete the original uploaded file // Delete the original uploaded file
try { fileDelete(uploadedFile); } catch (any e) {} try { fileDelete(uploadedFile); } catch (any e) {}

View file

@ -103,7 +103,7 @@ try {
// Create new business with address link and phone // Create new business with address link and phone
queryTimed(" queryTimed("
INSERT INTO Businesses (Name, Phone, UserID, AddressID, DeliveryZIPCodes, CommunityMealType, TaxRate, AddedOn) INSERT INTO Businesses (Name, Phone, UserID, AddressID, DeliveryZIPCodes, BusinessCommunityMealType, TaxRate, BusinessAddedOn)
VALUES (:name, :phone, :userId, :addressId, :deliveryZips, :communityMealType, :taxRate, NOW()) VALUES (:name, :phone, :userId, :addressId, :deliveryZips, :communityMealType, :taxRate, NOW())
", { ", {
name: bizName, name: bizName,

View file

@ -58,15 +58,21 @@ try {
} }
} }
// Get task type name if TaskTypeID provided // Get task type info if TaskTypeID provided (name + category)
taskTypeName = ""; taskTypeName = "";
taskTypeCategoryID = 0;
if (taskTypeID > 0) { if (taskTypeID > 0) {
typeQuery = queryExecute(" typeQuery = queryExecute("
SELECT Name FROM tt_TaskTypes WHERE ID = :typeID SELECT Name, TaskCategoryID FROM tt_TaskTypes WHERE ID = :typeID
", { typeID: { value: taskTypeID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); ", { typeID: { value: taskTypeID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
if (typeQuery.recordCount && len(trim(typeQuery.Name))) { if (typeQuery.recordCount) {
if (len(trim(typeQuery.Name))) {
taskTypeName = typeQuery.Name; taskTypeName = typeQuery.Name;
} }
if (!isNull(typeQuery.TaskCategoryID) && isNumeric(typeQuery.TaskCategoryID) && typeQuery.TaskCategoryID > 0) {
taskTypeCategoryID = typeQuery.TaskCategoryID;
}
}
} }
// Create task title and details - use task type name if available // Create task title and details - use task type name if available
@ -90,12 +96,40 @@ try {
taskDetails &= "Customer is requesting assistance"; taskDetails &= "Customer is requesting assistance";
} }
// Insert task (no CategoryID - using TaskTypeID only) // Determine category: use task type's category if set, otherwise fallback to "Service" category
categoryID = 0;
if (taskTypeCategoryID > 0) {
// Use the task type's assigned category
categoryID = taskTypeCategoryID;
} else {
// Fallback: look up or create a "Service" category for this business
catQuery = queryExecute("
SELECT ID FROM TaskCategories
WHERE BusinessID = :businessID AND Name = 'Service'
LIMIT 1
", { businessID: { value: businessID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
if (catQuery.recordCount == 0) {
// Create the category
queryExecute("
INSERT INTO TaskCategories (BusinessID, Name, Color)
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.ID;
}
}
// Insert task
queryExecute(" queryExecute("
INSERT INTO Tasks ( INSERT INTO Tasks (
BusinessID, BusinessID,
ServicePointID,
UserID, UserID,
CategoryID,
OrderID, OrderID,
TaskTypeID, TaskTypeID,
Title, Title,
@ -104,8 +138,8 @@ try {
CreatedOn CreatedOn
) VALUES ( ) VALUES (
:businessID, :businessID,
:servicePointID,
:userID, :userID,
:categoryID,
:orderID, :orderID,
:taskTypeID, :taskTypeID,
:title, :title,
@ -115,8 +149,8 @@ try {
) )
", { ", {
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }, businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
servicePointID: { value: servicePointID, cfsqltype: "cf_sql_integer" },
userID: { value: userID > 0 ? userID : javaCast("null", ""), cfsqltype: "cf_sql_integer", null: userID == 0 }, userID: { value: userID > 0 ? userID : javaCast("null", ""), cfsqltype: "cf_sql_integer", null: userID == 0 },
categoryID: { value: categoryID, cfsqltype: "cf_sql_integer" },
orderID: { value: orderID > 0 ? orderID : javaCast("null", ""), cfsqltype: "cf_sql_integer", null: orderID == 0 }, orderID: { value: orderID > 0 ? orderID : javaCast("null", ""), cfsqltype: "cf_sql_integer", null: orderID == 0 },
taskTypeID: { value: taskTypeID, cfsqltype: "cf_sql_integer" }, taskTypeID: { value: taskTypeID, cfsqltype: "cf_sql_integer" },
title: { value: taskTitle, cfsqltype: "cf_sql_varchar" }, title: { value: taskTitle, cfsqltype: "cf_sql_varchar" },

View file

@ -29,25 +29,55 @@ try {
userID = val(jsonData.UserID ?: 0); userID = val(jsonData.UserID ?: 0);
message = jsonData.Message ?: ""; message = jsonData.Message ?: "";
// Get task type name for display // Get task type info for display (including category)
taskTypeQuery = queryTimed(" taskTypeQuery = queryTimed("
SELECT Name FROM tt_TaskTypes WHERE ID = :taskTypeID SELECT Name, Color, Icon, TaskCategoryID
FROM tt_TaskTypes
WHERE ID = :taskTypeID
", { taskTypeID: taskTypeID }, { datasource: "payfrit" }); ", { taskTypeID: taskTypeID }, { datasource: "payfrit" });
taskTitle = message; taskTitle = message;
if (taskTypeQuery.recordCount && len(trim(taskTypeQuery.Name))) { categoryID = 0;
if (taskTypeQuery.recordCount) {
if (len(trim(taskTypeQuery.Name))) {
taskTitle = taskTypeQuery.Name; taskTitle = taskTypeQuery.Name;
} }
if (!isNull(taskTypeQuery.TaskCategoryID) && isNumeric(taskTypeQuery.TaskCategoryID) && taskTypeQuery.TaskCategoryID > 0) {
categoryID = taskTypeQuery.TaskCategoryID;
}
}
// If no category from task type, look up or create default "Service" category
if (categoryID == 0) {
catQuery = queryTimed("
SELECT ID FROM TaskCategories
WHERE BusinessID = :businessID AND Name = 'Service'
LIMIT 1
", { businessID: businessID }, { datasource: "payfrit" });
if (catQuery.recordCount > 0) {
categoryID = catQuery.ID;
} else {
// Create the Service category
queryTimed("
INSERT INTO TaskCategories (BusinessID, Name, Color)
VALUES (:businessID, 'Service', '##FF9800')
", { businessID: businessID }, { datasource: "payfrit" });
catResult = queryTimed("SELECT LAST_INSERT_ID() as newID", {}, { datasource: "payfrit" });
categoryID = catResult.newID;
}
}
// Use message as details // Use message as details
taskDetails = message; taskDetails = message;
// Insert service bell task (no CategoryID - using TaskTypeID only) // Insert service bell task with ServicePointID and UserID
queryTimed(" queryTimed("
INSERT INTO Tasks ( INSERT INTO Tasks (
BusinessID, BusinessID,
ServicePointID, ServicePointID,
TaskTypeID, TaskTypeID,
CategoryID,
OrderID, OrderID,
UserID, UserID,
Title, Title,
@ -58,6 +88,7 @@ try {
:businessID, :businessID,
:servicePointID, :servicePointID,
:taskTypeID, :taskTypeID,
:categoryID,
:orderID, :orderID,
:userID, :userID,
:taskTitle, :taskTitle,
@ -69,6 +100,7 @@ try {
businessID: businessID, businessID: businessID,
servicePointID: servicePointID, servicePointID: servicePointID,
taskTypeID: taskTypeID, taskTypeID: taskTypeID,
categoryID: categoryID,
orderID: orderID, orderID: orderID,
userID: userID, userID: userID,
taskTitle: taskTitle, taskTitle: taskTitle,

View file

@ -34,18 +34,20 @@
</cfif> </cfif>
<cftry> <cftry>
<!--- Get the task and linked order details (no categories - using task types only) ---> <!--- Get the task and linked order details --->
<cfset qTask = queryExecute(" <cfset qTask = queryExecute("
SELECT SELECT
t.ID AS TaskID, t.ID AS TaskID,
t.BusinessID, t.BusinessID,
t.CategoryID,
t.OrderID, t.OrderID,
t.TaskTypeID, t.TaskTypeID,
t.CreatedOn, t.CreatedOn,
t.ClaimedByUserID, t.ClaimedByUserID,
t.ServicePointID AS TaskServicePointID, t.ServicePointID AS TaskServicePointID,
tc.Name AS CategoryName,
tc.Color AS CategoryColor,
tt.Name AS TaskTypeName, tt.Name AS TaskTypeName,
tt.Color AS TaskTypeColor,
o.ID AS OID, o.ID AS OID,
o.UUID AS OrderUUID, o.UUID AS OrderUUID,
o.UserID AS OrderUserID, o.UserID AS OrderUserID,
@ -66,6 +68,7 @@
u.ContactNumber, u.ContactNumber,
u.ImageExtension AS CustomerImageExtension u.ImageExtension AS CustomerImageExtension
FROM Tasks t FROM Tasks t
LEFT JOIN TaskCategories tc ON tc.ID = t.CategoryID
LEFT JOIN tt_TaskTypes tt ON tt.ID = t.TaskTypeID 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 Businesses b ON b.ID = t.BusinessID
@ -104,12 +107,14 @@
<cfset result = { <cfset result = {
"TaskID": qTask.TaskID, "TaskID": qTask.TaskID,
"TaskBusinessID": qTask.BusinessID, "TaskBusinessID": qTask.BusinessID,
"TaskCategoryID": qTask.CategoryID,
"TaskTypeID": qTask.TaskTypeID ?: 1, "TaskTypeID": qTask.TaskTypeID ?: 1,
"TaskTypeName": qTask.TaskTypeName ?: "", "TaskTypeName": qTask.TaskTypeName ?: "",
"TaskTypeColor": len(trim(qTask.TaskTypeColor)) ? qTask.TaskTypeColor : "##9C27B0",
"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,
"TaskCategoryName": len(trim(qTask.CategoryName)) ? qTask.CategoryName : "General",
"TaskCategoryColor": len(trim(qTask.CategoryColor)) ? qTask.CategoryColor : "##888888",
"OrderID": qTask.OrderID ?: 0, "OrderID": qTask.OrderID ?: 0,
"OrderRemarks": qTask.Remarks ?: "", "OrderRemarks": qTask.Remarks ?: "",
"OrderSubmittedOn": isDate(qTask.SubmittedOn) ? (dateFormat(qTask.SubmittedOn, "yyyy-mm-dd") & "T" & timeFormat(qTask.SubmittedOn, "HH:mm:ss")) : "", "OrderSubmittedOn": isDate(qTask.SubmittedOn) ? (dateFormat(qTask.SubmittedOn, "yyyy-mm-dd") & "T" & timeFormat(qTask.SubmittedOn, "HH:mm:ss")) : "",

View file

@ -68,6 +68,7 @@
SELECT SELECT
t.ID, t.ID,
t.BusinessID, t.BusinessID,
t.CategoryID,
t.OrderID, t.OrderID,
t.TaskTypeID, t.TaskTypeID,
t.Title, t.Title,
@ -77,6 +78,8 @@
t.ClaimedOn, t.ClaimedOn,
t.CompletedOn, t.CompletedOn,
t.UserID, t.UserID,
tc.Name AS CategoryName,
tc.Color AS CategoryColor,
tt.Name AS TaskTypeName, tt.Name AS TaskTypeName,
tt.Icon AS TaskTypeIcon, tt.Icon AS TaskTypeIcon,
tt.Color AS TaskTypeColor, tt.Color AS TaskTypeColor,
@ -86,6 +89,7 @@
u.FirstName AS CustomerFirstName, u.FirstName AS CustomerFirstName,
u.LastName AS CustomerLastName u.LastName AS CustomerLastName
FROM Tasks t FROM Tasks t
LEFT JOIN TaskCategories tc ON tc.ID = t.CategoryID
LEFT JOIN tt_TaskTypes tt ON tt.ID = t.TaskTypeID LEFT JOIN tt_TaskTypes tt ON tt.ID = t.TaskTypeID
LEFT JOIN Businesses b ON b.ID = t.BusinessID LEFT JOIN Businesses b ON b.ID = t.BusinessID
LEFT JOIN Orders o ON o.ID = t.OrderID LEFT JOIN Orders o ON o.ID = t.OrderID
@ -125,6 +129,7 @@
"TaskID": qTasks.ID, "TaskID": qTasks.ID,
"BusinessID": qTasks.BusinessID, "BusinessID": qTasks.BusinessID,
"BusinessName": qTasks.BusinessName ?: "", "BusinessName": qTasks.BusinessName ?: "",
"TaskCategoryID": qTasks.CategoryID,
"TaskTypeID": qTasks.TaskTypeID, "TaskTypeID": qTasks.TaskTypeID,
"Title": taskTitle, "Title": taskTitle,
"Details": taskDetails, "Details": taskDetails,
@ -134,6 +139,8 @@
"StatusID": (isNull(qTasks.CompletedOn) OR len(trim(qTasks.CompletedOn)) EQ 0) ? 1 : 3, "StatusID": (isNull(qTasks.CompletedOn) OR len(trim(qTasks.CompletedOn)) EQ 0) ? 1 : 3,
"SourceType": "order", "SourceType": "order",
"SourceID": qTasks.OrderID, "SourceID": qTasks.OrderID,
"CategoryName": len(trim(qTasks.CategoryName)) ? qTasks.CategoryName : "General",
"CategoryColor": len(trim(qTasks.CategoryColor)) ? qTasks.CategoryColor : "##888888",
"TaskTypeName": len(trim(qTasks.TaskTypeName)) ? qTasks.TaskTypeName : "", "TaskTypeName": len(trim(qTasks.TaskTypeName)) ? qTasks.TaskTypeName : "",
"TaskTypeIcon": len(trim(qTasks.TaskTypeIcon)) ? qTasks.TaskTypeIcon : "notifications", "TaskTypeIcon": len(trim(qTasks.TaskTypeIcon)) ? qTasks.TaskTypeIcon : "notifications",
"TaskTypeColor": len(trim(qTasks.TaskTypeColor)) ? qTasks.TaskTypeColor : "##9C27B0", "TaskTypeColor": len(trim(qTasks.TaskTypeColor)) ? qTasks.TaskTypeColor : "##9C27B0",

View file

@ -28,7 +28,7 @@
<cfset data = readJsonBody()> <cfset data = readJsonBody()>
<cfset BusinessID = val( structKeyExists(data,"BusinessID") ? data.BusinessID : 0 )> <cfset BusinessID = val( structKeyExists(data,"BusinessID") ? data.BusinessID : 0 )>
<cfset TaskTypeID = val( structKeyExists(data,"TaskTypeID") ? data.TaskTypeID : 0 )> <cfset CategoryID = val( structKeyExists(data,"CategoryID") ? data.CategoryID : 0 )>
<cfif BusinessID LTE 0> <cfif BusinessID LTE 0>
<cfset apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required." })> <cfset apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required." })>
@ -46,10 +46,10 @@
<cfset whereClauses = ["t.BusinessID = ?", "t.ClaimedByUserID = 0", "t.CompletedOn IS NULL"]> <cfset whereClauses = ["t.BusinessID = ?", "t.ClaimedByUserID = 0", "t.CompletedOn IS NULL"]>
<cfset params = [ { value = BusinessID, cfsqltype = "cf_sql_integer" } ]> <cfset params = [ { value = BusinessID, cfsqltype = "cf_sql_integer" } ]>
<!--- Filter by task type if provided ---> <!--- Filter by category if provided --->
<cfif TaskTypeID GT 0> <cfif CategoryID GT 0>
<cfset arrayAppend(whereClauses, "t.TaskTypeID = ?")> <cfset arrayAppend(whereClauses, "t.CategoryID = ?")>
<cfset arrayAppend(params, { value = TaskTypeID, cfsqltype = "cf_sql_integer" })> <cfset arrayAppend(params, { value = CategoryID, cfsqltype = "cf_sql_integer" })>
</cfif> </cfif>
<cfset whereSQL = arrayToList(whereClauses, " AND ")> <cfset whereSQL = arrayToList(whereClauses, " AND ")>
@ -58,6 +58,7 @@
SELECT SELECT
t.ID, t.ID,
t.BusinessID, t.BusinessID,
t.CategoryID,
t.OrderID, t.OrderID,
t.TaskTypeID, t.TaskTypeID,
t.Title, t.Title,
@ -65,6 +66,8 @@
t.CreatedOn, t.CreatedOn,
t.ClaimedByUserID, t.ClaimedByUserID,
t.UserID, t.UserID,
tc.Name AS CategoryName,
tc.Color AS CategoryColor,
tt.Name AS TaskTypeName, tt.Name AS TaskTypeName,
tt.Icon AS TaskTypeIcon, tt.Icon AS TaskTypeIcon,
tt.Color AS TaskTypeColor, tt.Color AS TaskTypeColor,
@ -73,6 +76,7 @@
u.FirstName AS CustomerFirstName, u.FirstName AS CustomerFirstName,
u.LastName AS CustomerLastName u.LastName AS CustomerLastName
FROM Tasks t FROM Tasks t
LEFT JOIN TaskCategories tc ON tc.ID = t.CategoryID
LEFT JOIN tt_TaskTypes tt ON tt.ID = t.TaskTypeID 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 ServicePoints sp ON sp.ID = COALESCE(t.ServicePointID, o.ServicePointID) LEFT JOIN ServicePoints sp ON sp.ID = COALESCE(t.ServicePointID, o.ServicePointID)
@ -107,6 +111,7 @@
<cfset arrayAppend(tasks, { <cfset arrayAppend(tasks, {
"TaskID": qTasks.ID, "TaskID": qTasks.ID,
"BusinessID": qTasks.BusinessID, "BusinessID": qTasks.BusinessID,
"TaskCategoryID": qTasks.CategoryID,
"TaskTypeID": qTasks.TaskTypeID, "TaskTypeID": qTasks.TaskTypeID,
"Title": taskTitle, "Title": taskTitle,
"Details": taskDetails, "Details": taskDetails,
@ -114,6 +119,8 @@
"StatusID": qTasks.ClaimedByUserID GT 0 ? 1 : 0, "StatusID": qTasks.ClaimedByUserID GT 0 ? 1 : 0,
"SourceType": "order", "SourceType": "order",
"SourceID": qTasks.OrderID, "SourceID": qTasks.OrderID,
"CategoryName": len(trim(qTasks.CategoryName)) ? qTasks.CategoryName : "General",
"CategoryColor": len(trim(qTasks.CategoryColor)) ? qTasks.CategoryColor : "##888888",
"TaskTypeName": len(trim(qTasks.TaskTypeName)) ? qTasks.TaskTypeName : "", "TaskTypeName": len(trim(qTasks.TaskTypeName)) ? qTasks.TaskTypeName : "",
"TaskTypeIcon": len(trim(qTasks.TaskTypeIcon)) ? qTasks.TaskTypeIcon : "notifications", "TaskTypeIcon": len(trim(qTasks.TaskTypeIcon)) ? qTasks.TaskTypeIcon : "notifications",
"TaskTypeColor": len(trim(qTasks.TaskTypeColor)) ? qTasks.TaskTypeColor : "##9C27B0", "TaskTypeColor": len(trim(qTasks.TaskTypeColor)) ? qTasks.TaskTypeColor : "##9C27B0",

View file

@ -41,14 +41,13 @@
MIN(e.StatusID) AS StatusID, MIN(e.StatusID) AS StatusID,
MAX(e.IsActive) AS IsActive, MAX(e.IsActive) AS IsActive,
b.Name AS Name, b.Name AS Name,
(SELECT COUNT(*) FROM Tasks t WHERE t.BusinessID = e.BusinessID AND t.ClaimedByUserID = 0 AND t.CompletedOn IS NULL) AS PendingTaskCount, (SELECT COUNT(*) FROM Tasks t WHERE t.BusinessID = e.BusinessID AND t.ClaimedByUserID = 0 AND t.CompletedOn IS NULL) AS PendingTaskCount
(SELECT COUNT(*) FROM Tasks t WHERE t.BusinessID = e.BusinessID AND t.ClaimedByUserID = ? AND t.CompletedOn IS NULL) AS ActiveTaskCount
FROM Employees e FROM Employees e
INNER JOIN Businesses b ON b.ID = e.BusinessID INNER JOIN Businesses b ON b.ID = e.BusinessID
WHERE e.UserID = ? AND e.IsActive = b'1' WHERE e.UserID = ? AND e.IsActive = b'1'
GROUP BY e.BusinessID, b.Name GROUP BY e.BusinessID, b.Name
ORDER BY b.Name ASC ORDER BY b.Name ASC
", [ { value = UserID, cfsqltype = "cf_sql_integer" }, { value = UserID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })> ", [ { value = UserID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<cfset businesses = []> <cfset businesses = []>
@ -60,8 +59,7 @@
"Address": "", "Address": "",
"City": "", "City": "",
"StatusID": qBusinesses.StatusID, "StatusID": qBusinesses.StatusID,
"PendingTaskCount": qBusinesses.PendingTaskCount, "PendingTaskCount": qBusinesses.PendingTaskCount
"ActiveTaskCount": qBusinesses.ActiveTaskCount
})> })>
</cfloop> </cfloop>

View file

@ -225,16 +225,13 @@ const HUD = {
// Get elapsed seconds since task creation // Get elapsed seconds since task creation
getElapsedSeconds(createdOn) { getElapsedSeconds(createdOn) {
if (!createdOn) return 0;
const created = new Date(createdOn); const created = new Date(createdOn);
if (isNaN(created.getTime())) return 0;
const now = new Date(); const now = new Date();
return Math.max(0, Math.floor((now - created) / 1000)); return Math.floor((now - created) / 1000);
}, },
// Format elapsed time as mm:ss // Format elapsed time as mm:ss
formatElapsed(seconds) { formatElapsed(seconds) {
if (isNaN(seconds) || seconds < 0) seconds = 0;
const mins = Math.floor(seconds / 60); const mins = Math.floor(seconds / 60);
const secs = seconds % 60; const secs = seconds % 60;
return `${mins}:${String(secs).padStart(2, '0')}`; return `${mins}:${String(secs).padStart(2, '0')}`;

View file

@ -4,14 +4,772 @@
<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>Login - Payfrit Business Portal</title> <title>Login - Payfrit Business Portal</title>
<script> <link rel="stylesheet" href="portal.css">
// Redirect to signup.html - single auth entry point <style>
// Existing users verified by phone get their existing account body {
// New users go through full setup flow background: var(--bg-primary);
window.location.replace('signup.html'); min-height: 100vh;
</script> display: flex;
align-items: center;
justify-content: center;
}
.login-container {
width: 100%;
max-width: 420px;
padding: 20px;
}
.login-card {
background: var(--bg-card);
border-radius: 16px;
padding: 40px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
}
.login-header {
text-align: center;
margin-bottom: 32px;
}
.login-logo {
width: 64px;
height: 64px;
background: var(--primary);
color: #000;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
font-weight: 700;
margin: 0 auto 16px;
}
.login-header h1 {
font-size: 24px;
margin: 0 0 8px;
color: var(--text-primary);
}
.login-header p {
color: var(--text-muted);
margin: 0;
font-size: 14px;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
}
.form-group input {
padding: 14px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
font-size: 15px;
transition: all 0.2s;
}
.form-group input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1);
}
.form-group input::placeholder {
color: var(--text-muted);
}
.login-btn {
padding: 14px 24px;
background: var(--primary);
color: #000;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-top: 8px;
}
.login-btn:hover {
background: var(--primary-hover);
transform: translateY(-1px);
}
.login-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.login-error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #ef4444;
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
display: none;
}
.login-error.show {
display: block;
}
.login-success {
background: rgba(0, 255, 136, 0.1);
border: 1px solid rgba(0, 255, 136, 0.3);
color: var(--primary);
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
display: none;
}
.login-success.show {
display: block;
}
.login-footer {
text-align: center;
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid var(--border-color);
}
.login-footer a {
color: var(--primary);
text-decoration: none;
font-size: 14px;
}
.login-footer a:hover {
text-decoration: underline;
}
.business-select {
padding: 14px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
font-size: 15px;
width: 100%;
cursor: pointer;
}
.business-select:focus {
outline: none;
border-color: var(--primary);
}
.login-card {
visibility: hidden;
}
.login-card.ready {
visibility: visible;
}
.step {
display: none;
}
.step.active {
display: block;
}
.otp-input {
text-align: center;
font-size: 24px;
font-weight: 600;
letter-spacing: 8px;
padding: 14px 16px;
}
.switch-link {
color: var(--text-muted);
text-decoration: none;
font-size: 13px;
cursor: pointer;
}
.switch-link:hover {
color: var(--primary);
text-decoration: underline;
}
.resend-link {
color: var(--text-muted);
font-size: 13px;
cursor: pointer;
background: none;
border: none;
padding: 0;
text-decoration: none;
}
.resend-link:hover {
color: var(--primary);
text-decoration: underline;
}
.resend-link:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head> </head>
<body> <body>
<p>Redirecting...</p> <div class="login-container">
<div class="login-card">
<div class="login-header">
<div class="login-logo">P</div>
<h1>Business Portal</h1>
<p id="loginSubtitle">Sign in with your email</p>
</div>
<!-- Step 1: OTP - Enter Email -->
<div class="step" id="step-otp-email">
<form class="login-form" id="otpEmailForm">
<div class="login-error" id="otpEmailError"></div>
<div class="form-group">
<label for="otpEmail">Email Address</label>
<input type="email" id="otpEmail" name="email" placeholder="Enter your email" required>
</div>
<button type="submit" class="login-btn" id="sendCodeBtn">Send Login Code</button>
</form>
<div class="login-footer">
<a href="#" class="switch-link" onclick="PortalLogin.showStep('password'); return false;">Sign in with password instead</a>
</div>
</div>
<!-- Step 2: OTP - Enter Code -->
<div class="step" id="step-otp-code">
<form class="login-form" id="otpCodeForm">
<div class="login-error" id="otpCodeError"></div>
<div class="login-success" id="otpCodeSuccess">A 6-digit code has been sent to your email.</div>
<div class="form-group">
<label for="otpCode">Enter 6-Digit Code</label>
<input type="text" id="otpCode" name="code" class="otp-input" placeholder="000000" maxlength="6" pattern="[0-9]{6}" inputmode="numeric" autocomplete="one-time-code" required>
</div>
<button type="submit" class="login-btn" id="verifyCodeBtn">Verify Code</button>
</form>
<div class="login-footer" style="display: flex; justify-content: space-between; align-items: center;">
<a href="#" class="switch-link" onclick="PortalLogin.showStep('otp-email'); return false;">Change email</a>
<button class="resend-link" id="resendBtn" onclick="PortalLogin.resendCode()">Resend code</button>
</div>
</div>
<!-- Step 3: Password Login (fallback) -->
<div class="step" id="step-password">
<form class="login-form" id="loginForm">
<div class="login-error" id="loginError"></div>
<div class="form-group">
<label for="username">Email or Phone</label>
<input type="text" id="username" name="username" placeholder="Enter your email or phone" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" placeholder="Enter your password" required>
</div>
<button type="submit" class="login-btn" id="loginBtn">Sign In</button>
</form>
<div class="login-footer">
<a href="#" class="switch-link" onclick="PortalLogin.showStep('otp-email'); return false;">Sign in with email code instead</a>
</div>
</div>
<!-- Step 4: Select Business -->
<div class="step" id="step-business">
<div class="login-form">
<div class="form-group">
<label for="businessSelect">Select Business</label>
<select id="businessSelect" class="business-select" onchange="PortalLogin.selectBusiness()">
<option value="">Choose a business...</option>
</select>
</div>
</div>
</div>
</div>
</div>
<script>
const BASE_PATH = (() => {
const path = window.location.pathname;
const portalIndex = path.indexOf('/portal/');
if (portalIndex > 0) {
return path.substring(0, portalIndex);
}
return '';
})();
const PortalLogin = {
userToken: null,
userId: null,
businesses: [],
otpEmail: null,
resendCooldown: false,
init() {
const card = document.querySelector('.login-card');
const savedToken = localStorage.getItem('payfrit_portal_token');
const savedBusiness = localStorage.getItem('payfrit_portal_business');
const savedUserId = localStorage.getItem('payfrit_portal_userid');
if (savedToken && savedBusiness) {
this.verifyAndRedirect(savedToken, savedBusiness);
return;
}
if (savedToken && savedUserId) {
this.userToken = savedToken;
this.userId = parseInt(savedUserId);
this.showStep('business');
card.classList.add('ready');
this.loadBusinessesAndShow();
return;
}
// Default: show OTP email step
this.showStep('otp-email');
card.classList.add('ready');
// OTP email form
document.getElementById('otpEmailForm').addEventListener('submit', (e) => {
e.preventDefault();
this.sendOTP();
});
// OTP code form
document.getElementById('otpCodeForm').addEventListener('submit', (e) => {
e.preventDefault();
this.verifyOTP();
});
// Password login form
document.getElementById('loginForm').addEventListener('submit', (e) => {
e.preventDefault();
this.login();
});
},
async loadBusinessesAndShow() {
await this.loadBusinesses();
if (this.businesses.length === 0) {
this.startNewRestaurant();
} else if (this.businesses.length === 1) {
this.selectBusinessById(this.businesses[0].BusinessID);
} else {
this.showStep('business');
}
},
async verifyAndRedirect(token, businessId) {
localStorage.setItem('payfrit_portal_business', businessId);
window.location.href = BASE_PATH + '/portal/index.html';
},
// --- OTP Flow ---
async sendOTP() {
const email = document.getElementById('otpEmail').value.trim();
const errorEl = document.getElementById('otpEmailError');
const btn = document.getElementById('sendCodeBtn');
if (!email) return;
errorEl.classList.remove('show');
btn.disabled = true;
btn.textContent = 'Sending...';
try {
const response = await fetch(BASE_PATH + '/api/auth/sendLoginOTP.cfm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ Email: email })
});
const text = await response.text();
let data;
try {
data = JSON.parse(text);
} catch (parseErr) {
console.error('[OTP] JSON parse error:', parseErr);
errorEl.textContent = 'Server error. Please try again.';
errorEl.classList.add('show');
btn.disabled = false;
btn.textContent = 'Send Login Code';
return;
}
if (data.OK) {
this.otpEmail = email;
// On dev, show the code for testing
if (data.DEV_OTP) {
console.log('[OTP] Dev code:', data.DEV_OTP);
document.getElementById('otpCodeSuccess').textContent =
'Dev mode - your code is: ' + data.DEV_OTP;
} else {
document.getElementById('otpCodeSuccess').textContent =
'A 6-digit code has been sent to ' + email;
}
document.getElementById('otpCodeSuccess').classList.add('show');
this.showStep('otp-code');
document.getElementById('otpCode').focus();
} else {
errorEl.textContent = data.MESSAGE || data.ERROR || 'Could not send code. Please try again.';
errorEl.classList.add('show');
}
} catch (err) {
console.error('[OTP] Send error:', err);
errorEl.textContent = 'Connection error. Please try again.';
errorEl.classList.add('show');
}
btn.disabled = false;
btn.textContent = 'Send Login Code';
},
async verifyOTP() {
const code = document.getElementById('otpCode').value.trim();
const errorEl = document.getElementById('otpCodeError');
const btn = document.getElementById('verifyCodeBtn');
if (!code || code.length !== 6) {
errorEl.textContent = 'Please enter the 6-digit code.';
errorEl.classList.add('show');
return;
}
errorEl.classList.remove('show');
btn.disabled = true;
btn.textContent = 'Verifying...';
try {
const response = await fetch(BASE_PATH + '/api/auth/verifyEmailOTP.cfm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ Email: this.otpEmail, Code: code })
});
const text = await response.text();
let data;
try {
data = JSON.parse(text);
} catch (parseErr) {
console.error('[OTP] Verify parse error:', parseErr);
errorEl.textContent = 'Server error. Please try again.';
errorEl.classList.add('show');
btn.disabled = false;
btn.textContent = 'Verify Code';
return;
}
if (data.OK && data.TOKEN) {
// Normalize key casing
const token = data.TOKEN || data.Token;
const userId = data.USERID || data.UserID;
this.userToken = token;
this.userId = userId;
localStorage.setItem('payfrit_portal_token', token);
localStorage.setItem('payfrit_portal_userid', userId);
const firstName = data.FIRSTNAME || data.FirstName || '';
if (firstName) localStorage.setItem('payfrit_portal_firstname', firstName);
await this.loadBusinesses();
const card = document.querySelector('.login-card');
if (this.businesses.length === 0) {
this.startNewRestaurant();
} else if (this.businesses.length === 1) {
this.selectBusinessById(this.businesses[0].BusinessID);
} else {
this.showStep('business');
card.classList.add('ready');
}
} else {
const friendlyErrors = {
'invalid_code': 'Invalid or expired code. Please try again.',
'expired': 'Code has expired. Please request a new one.',
'user_not_found': 'No account found with that email.'
};
errorEl.textContent = friendlyErrors[data.ERROR] || data.MESSAGE || 'Invalid code. Please try again.';
errorEl.classList.add('show');
document.getElementById('otpCode').value = '';
document.getElementById('otpCode').focus();
}
} catch (err) {
console.error('[OTP] Verify error:', err);
errorEl.textContent = 'Connection error. Please try again.';
errorEl.classList.add('show');
}
btn.disabled = false;
btn.textContent = 'Verify Code';
},
async resendCode() {
if (this.resendCooldown) return;
const btn = document.getElementById('resendBtn');
this.resendCooldown = true;
btn.disabled = true;
// Re-send OTP
try {
const response = await fetch(BASE_PATH + '/api/auth/sendLoginOTP.cfm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ Email: this.otpEmail })
});
const text = await response.text();
let data;
try { data = JSON.parse(text); } catch (e) { data = {}; }
const successEl = document.getElementById('otpCodeSuccess');
if (data.OK) {
if (data.DEV_OTP) {
successEl.textContent = 'Dev mode - new code: ' + data.DEV_OTP;
} else {
successEl.textContent = 'A new code has been sent to ' + this.otpEmail;
}
successEl.classList.add('show');
}
} catch (err) {
console.error('[OTP] Resend error:', err);
}
// 30-second cooldown
let seconds = 30;
btn.textContent = 'Resend (' + seconds + 's)';
const interval = setInterval(() => {
seconds--;
if (seconds <= 0) {
clearInterval(interval);
btn.textContent = 'Resend code';
btn.disabled = false;
this.resendCooldown = false;
} else {
btn.textContent = 'Resend (' + seconds + 's)';
}
}, 1000);
},
// --- Password Login (fallback) ---
async login() {
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
const errorEl = document.getElementById('loginError');
const btn = document.getElementById('loginBtn');
errorEl.classList.remove('show');
btn.disabled = true;
btn.textContent = 'Signing in...';
try {
const response = await fetch(BASE_PATH + '/api/auth/login.cfm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const text = await response.text();
let data;
try {
data = JSON.parse(text);
} catch (parseErr) {
console.error("[Login] JSON parse error:", parseErr);
errorEl.textContent = "Server error - check browser console (F12)";
errorEl.classList.add("show");
btn.disabled = false;
btn.textContent = "Sign In";
return;
}
if (data.OK && (data.Token || data.TOKEN)) {
const token = data.Token || data.TOKEN;
const userId = data.UserID || data.USERID;
this.userToken = token;
this.userId = userId;
localStorage.setItem('payfrit_portal_token', token);
localStorage.setItem('payfrit_portal_userid', userId);
const firstName = data.FirstName || data.FIRSTNAME || '';
if (firstName) localStorage.setItem('payfrit_portal_firstname', firstName);
await this.loadBusinesses();
const card = document.querySelector('.login-card');
if (this.businesses.length === 0) {
this.startNewRestaurant();
} else if (this.businesses.length === 1) {
this.selectBusinessById(this.businesses[0].BusinessID);
} else {
this.showStep('business');
card.classList.add('ready');
}
} else {
const friendlyErrors = {
'bad_credentials': 'Incorrect email/phone or password. Please try again.',
'not_found': 'No account found with that email or phone number.',
'account_disabled': 'This account has been disabled. Please contact support.'
};
errorEl.textContent = friendlyErrors[data.ERROR] || data.MESSAGE || 'Invalid credentials';
errorEl.classList.add('show');
}
} catch (err) {
console.error('[Login] Error:', err);
errorEl.textContent = 'Connection error. Please try again.';
errorEl.classList.add('show');
}
btn.disabled = false;
btn.textContent = 'Sign In';
},
// --- Business Selection ---
async loadBusinesses() {
try {
console.log('[Login] Loading businesses for UserID:', this.userId);
const bizResponse = await fetch(BASE_PATH + '/api/portal/myBusinesses.cfm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-User-Token': this.userToken
},
body: JSON.stringify({ UserID: this.userId })
});
const bizText = await bizResponse.text();
let bizData;
try {
bizData = JSON.parse(bizText);
} catch (parseErr) {
console.error("[Login] Businesses JSON parse error:", parseErr);
return;
}
if (bizData.OK && bizData.BUSINESSES) {
this.businesses = bizData.BUSINESSES;
console.log('[Login] Loaded', this.businesses.length, 'businesses');
this.populateBusinessSelect();
} else {
console.error('[Login] Businesses API error:', bizData.ERROR || bizData.MESSAGE);
}
} catch (err) {
console.error('[Login] Error loading businesses:', err);
}
},
populateBusinessSelect() {
const select = document.getElementById('businessSelect');
if (this.businesses.length === 0) {
select.innerHTML = '<option value="">No businesses yet</option>';
const wizardOption = document.createElement('option');
wizardOption.value = 'NEW_WIZARD';
wizardOption.textContent = 'Create New Business';
select.appendChild(wizardOption);
} else {
select.innerHTML = '<option value="">Choose a business...</option>';
this.businesses.forEach(biz => {
const option = document.createElement('option');
option.value = biz.BusinessID;
option.textContent = biz.Name;
select.appendChild(option);
});
const wizardOption = document.createElement('option');
wizardOption.value = 'NEW_WIZARD';
wizardOption.textContent = 'New Business Wizard';
select.appendChild(wizardOption);
}
},
showStep(step) {
document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
const el = document.getElementById('step-' + step);
if (el) el.classList.add('active');
// Update subtitle
const subtitle = document.getElementById('loginSubtitle');
if (step === 'otp-email') subtitle.textContent = 'Sign in with your email';
else if (step === 'otp-code') subtitle.textContent = 'Enter your verification code';
else if (step === 'password') subtitle.textContent = 'Sign in with your password';
else if (step === 'business') subtitle.textContent = 'Select your business';
// Clear errors when switching
document.querySelectorAll('.login-error').forEach(e => e.classList.remove('show'));
},
selectBusiness() {
const businessId = document.getElementById('businessSelect').value;
if (businessId === 'NEW_WIZARD') {
this.startNewRestaurant();
} else if (businessId) {
this.selectBusinessById(businessId);
}
},
selectBusinessById(businessId) {
localStorage.setItem('payfrit_portal_business', businessId);
window.location.href = BASE_PATH + '/portal/index.html';
},
startNewRestaurant() {
localStorage.removeItem('payfrit_portal_business');
window.location.href = BASE_PATH + '/portal/setup-wizard.html';
},
logout() {
localStorage.removeItem('payfrit_portal_token');
localStorage.removeItem('payfrit_portal_userid');
localStorage.removeItem('payfrit_portal_business');
localStorage.removeItem('payfrit_portal_firstname');
window.location.href = BASE_PATH + '/portal/login.html';
}
};
document.addEventListener('DOMContentLoaded', () => PortalLogin.init());
</script>
</body> </body>
</html> </html>

View file

@ -817,19 +817,14 @@
</head> </head>
<body> <body>
<div class="app"> <div class="app">
<!-- Sidebar --> <!-- Sidebar (without Payfrit header) -->
<aside class="sidebar" id="sidebar"> <aside class="sidebar" id="sidebar">
<div class="business-info" style="padding: 12px 16px; border-bottom: 1px solid var(--gray-200); display: flex; align-items: center; gap: 12px;"> <div class="business-info" style="padding: 12px 16px; border-bottom: 1px solid var(--gray-200);">
<div class="business-avatar" id="businessAvatar">B</div> <div class="business-avatar" id="businessAvatar">B</div>
<div class="business-details" style="flex: 1;"> <div class="business-details">
<div class="business-name" id="businessName">Loading...</div> <div class="business-name" id="businessName">Loading...</div>
<div class="business-status online">Online</div> <div class="business-status online">Online</div>
</div> </div>
<button class="sidebar-toggle" id="sidebarToggle" style="background: none; border: none; cursor: pointer; padding: 4px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12h18M3 6h18M3 18h18"/>
</svg>
</button>
</div> </div>
<nav class="sidebar-nav"> <nav class="sidebar-nav">
@ -875,14 +870,6 @@
</svg> </svg>
<span>Services</span> <span>Services</span>
</a> </a>
<a href="index.html#service-points" class="nav-item">
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M12 2v4M12 18v4M2 12h4M18 12h4"/>
<path d="M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
</svg>
<span>Service Points</span>
</a>
<a href="index.html#admin-tasks" class="nav-item"> <a href="index.html#admin-tasks" class="nav-item">
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/> <path d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
@ -901,37 +888,6 @@
<!-- Main Content --> <!-- Main Content -->
<main class="main-content"> <main class="main-content">
<header class="top-bar">
<div class="page-title">
<h1>Menu Builder</h1>
</div>
<div class="top-bar-actions">
<div class="user-menu" style="position: relative;">
<button class="user-btn" id="userBtn" onclick="var dd=document.getElementById('userDropdown'); dd.style.display = dd.style.display === 'none' ? 'block' : 'none';">
<span class="user-avatar" id="userAvatar">B</span>
</button>
<div class="user-dropdown" id="userDropdown" style="display:none; position:absolute; right:0; top:100%; margin-top:8px; background:var(--bg-card); border:1px solid var(--border-color); border-radius:8px; min-width:200px; box-shadow:0 4px 12px rgba(0,0,0,0.3); z-index:1000; overflow:hidden;">
<a href="index.html#settings" style="display:flex; align-items:center; gap:10px; padding:12px 16px; color:var(--text-primary); text-decoration:none; transition:background 0.15s;" onmouseover="this.style.background='var(--bg-secondary)'" onmouseout="this.style.background=''">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.6 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.6a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
Settings
</a>
<a href="#" onclick="MenuBuilder.switchBusiness(); document.getElementById('userDropdown').classList.remove('show'); return false;" style="display:flex; align-items:center; gap:10px; padding:12px 16px; color:var(--text-primary); text-decoration:none; transition:background 0.15s;" onmouseover="this.style.background='var(--bg-secondary)'" onmouseout="this.style.background=''">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 3h5v5M8 3H3v5M3 16v5h5M21 16v5h-5M7 12h10"/></svg>
Switch Business
</a>
<a href="#" onclick="MenuBuilder.addNewBusiness(); document.getElementById('userDropdown').classList.remove('show'); return false;" style="display:flex; align-items:center; gap:10px; padding:12px 16px; color:var(--text-primary); text-decoration:none; transition:background 0.15s;" onmouseover="this.style.background='var(--bg-secondary)'" onmouseout="this.style.background=''">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
Add New Business
</a>
<div style="border-top: 1px solid var(--border-color);"></div>
<a href="#" onclick="MenuBuilder.logout(); return false;" style="display:flex; align-items:center; gap:10px; padding:12px 16px; color:#ef4444; text-decoration:none; transition:background 0.15s;" onmouseover="this.style.background='var(--bg-secondary)'" onmouseout="this.style.background=''">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 17l5-5-5-5M21 12H9"/></svg>
Logout
</a>
</div>
</div>
</div>
</header>
<div class="builder-wrapper"> <div class="builder-wrapper">
<!-- Toolbar --> <!-- Toolbar -->
<div class="builder-toolbar"> <div class="builder-toolbar">
@ -1279,7 +1235,7 @@
const savedBusiness = localStorage.getItem('payfrit_portal_business'); const savedBusiness = localStorage.getItem('payfrit_portal_business');
if (!token || !savedBusiness) { if (!token || !savedBusiness) {
window.location.href = BASE_PATH + '/portal/signup.html'; window.location.href = BASE_PATH + '/portal/login.html';
return; return;
} }
@ -1323,18 +1279,7 @@
logout() { logout() {
localStorage.removeItem('payfrit_portal_token'); localStorage.removeItem('payfrit_portal_token');
localStorage.removeItem('payfrit_portal_business'); localStorage.removeItem('payfrit_portal_business');
window.location.href = BASE_PATH + '/portal/signup.html'; window.location.href = BASE_PATH + '/portal/login.html';
},
// Switch to a different business
switchBusiness() {
localStorage.removeItem('payfrit_portal_business');
window.location.href = BASE_PATH + '/portal/index.html';
},
// Add a new business
addNewBusiness() {
window.location.href = BASE_PATH + '/portal/setup.html';
}, },
// Load business info // Load business info
@ -1348,15 +1293,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;
const initial = biz.Name ? biz.Name.charAt(0).toUpperCase() : 'B';
// 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.Name; if (businessName) businessName.textContent = biz.Name;
if (businessAvatar) businessAvatar.textContent = initial; if (businessAvatar) businessAvatar.textContent = biz.Name ? biz.Name.charAt(0).toUpperCase() : 'B';
// Update top bar user avatar
const userAvatar = document.getElementById('userAvatar');
if (userAvatar) userAvatar.textContent = initial;
} }
} catch (err) { } catch (err) {
console.error('[MenuBuilder] Error loading business:', err); console.error('[MenuBuilder] Error loading business:', err);
@ -1500,15 +1441,8 @@
} }
}); });
document.addEventListener('click', (e) => { document.addEventListener('click', () => {
this.hideContextMenu(); this.hideContextMenu();
// Close user dropdown if clicking outside
const userDropdown = document.getElementById('userDropdown');
const userBtn = document.getElementById('userBtn');
if (userDropdown && !userDropdown.contains(e.target) && !userBtn.contains(e.target)) {
userDropdown.classList.remove('show');
userDropdown.style.display = 'none';
}
}); });
}, },

View file

@ -56,21 +56,21 @@ const Portal = {
const savedBusiness = localStorage.getItem('payfrit_portal_business'); const savedBusiness = localStorage.getItem('payfrit_portal_business');
const userId = localStorage.getItem('payfrit_portal_userid'); const userId = localStorage.getItem('payfrit_portal_userid');
if (!token) { if (!token || !savedBusiness) {
// No token - redirect to signup // Not logged in - redirect to login
localStorage.removeItem('payfrit_portal_token'); localStorage.removeItem('payfrit_portal_token');
localStorage.removeItem('payfrit_portal_userid'); localStorage.removeItem('payfrit_portal_userid');
localStorage.removeItem('payfrit_portal_business'); localStorage.removeItem('payfrit_portal_business');
window.location.href = BASE_PATH + '/portal/signup.html'; window.location.href = BASE_PATH + '/portal/login.html';
return; return;
} }
// Use saved business ID from localStorage (might be null) // Use saved business ID from localStorage
this.config.businessId = savedBusiness ? parseInt(savedBusiness) : null; this.config.businessId = parseInt(savedBusiness) || null;
this.config.userId = parseInt(userId) || 1; this.config.userId = parseInt(userId) || 1;
this.config.token = token; this.config.token = token;
// Verify user has access to this business (or get their businesses if none selected) // Verify user has access to this business
try { try {
const response = await fetch(`${this.config.apiBaseUrl}/portal/myBusinesses.cfm`, { const response = await fetch(`${this.config.apiBaseUrl}/portal/myBusinesses.cfm`, {
method: 'POST', method: 'POST',
@ -82,30 +82,18 @@ const Portal = {
}); });
const data = await response.json(); const data = await response.json();
if (data.OK && data.BUSINESSES && data.BUSINESSES.length > 0) { if (data.OK && data.BUSINESSES) {
if (!this.config.businessId) { const hasAccess = data.BUSINESSES.some(b => b.BusinessID === this.config.businessId);
// No business selected if (!hasAccess && data.BUSINESSES.length > 0) {
if (data.BUSINESSES.length === 1) { // User doesn't have access to requested business, use their first business
// Only one business - auto-select it
this.config.businessId = data.BUSINESSES[0].BusinessID; this.config.businessId = data.BUSINESSES[0].BusinessID;
localStorage.setItem('payfrit_portal_business', this.config.businessId); localStorage.setItem('payfrit_portal_business', this.config.businessId);
} else { } else if (!hasAccess) {
// Multiple businesses - show chooser // User has no businesses
this.showBusinessChooser(data.BUSINESSES); this.toast('No businesses associated with your account', 'error');
this.logout();
return; return;
} }
} else {
const hasAccess = data.BUSINESSES.some(b => b.BusinessID === this.config.businessId);
if (!hasAccess) {
// User doesn't have access to requested business - show chooser
this.showBusinessChooser(data.BUSINESSES);
return;
}
}
} else {
// User has no businesses - go to setup wizard
window.location.href = BASE_PATH + '/portal/setup-wizard.html';
return;
} }
} catch (err) { } catch (err) {
console.error('[Portal] Auth verification error:', err); console.error('[Portal] Auth verification error:', err);
@ -145,69 +133,14 @@ const Portal = {
localStorage.removeItem('payfrit_portal_token'); localStorage.removeItem('payfrit_portal_token');
localStorage.removeItem('payfrit_portal_userid'); localStorage.removeItem('payfrit_portal_userid');
localStorage.removeItem('payfrit_portal_business'); localStorage.removeItem('payfrit_portal_business');
window.location.href = BASE_PATH + '/portal/signup.html'; window.location.href = BASE_PATH + '/portal/login.html';
}, },
// Switch to a different business - show chooser modal // Switch to a different business (go back to login to select)
async switchBusiness() { switchBusiness() {
const token = localStorage.getItem('payfrit_portal_token'); // Clear current business selection but keep token
const userId = localStorage.getItem('payfrit_portal_userid'); localStorage.removeItem('payfrit_portal_business');
window.location.href = BASE_PATH + '/portal/login.html';
try {
const response = await fetch(`${this.config.apiBaseUrl}/portal/myBusinesses.cfm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-User-Token': token },
body: JSON.stringify({ UserID: userId })
});
const data = await response.json();
if (data.OK && data.BUSINESSES && data.BUSINESSES.length > 0) {
this.showBusinessChooser(data.BUSINESSES);
} else {
window.location.href = BASE_PATH + '/portal/setup-wizard.html';
}
} catch (err) {
console.error('[Portal] Failed to load businesses:', err);
}
},
showBusinessChooser(businesses) {
// Remove existing modal if any
const existing = document.getElementById('businessChooserModal');
if (existing) existing.remove();
const modal = document.createElement('div');
modal.id = 'businessChooserModal';
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999';
const content = document.createElement('div');
content.style.cssText = 'background:white;color:#333;border-radius:12px;padding:24px;max-width:400px;width:90%;max-height:80vh;overflow-y:auto';
content.innerHTML = `
<h2 style="margin:0 0 16px;font-size:18px;color:#333">Select Business</h2>
<div id="businessList"></div>
<button id="closeChooser" style="margin-top:16px;padding:8px 16px;background:#f0f0f0;color:#333;border:none;border-radius:6px;cursor:pointer;width:100%">Cancel</button>
`;
const list = content.querySelector('#businessList');
businesses.forEach(biz => {
const item = document.createElement('div');
item.style.cssText = 'padding:12px;border:1px solid #e0e0e0;border-radius:8px;margin-bottom:8px;cursor:pointer;transition:background 0.2s;color:#333;background:white';
item.innerHTML = `<strong>${biz.BusinessName || biz.Name}</strong>`;
item.onmouseover = () => item.style.background = '#f5f5f5';
item.onmouseout = () => item.style.background = 'white';
item.onclick = () => {
localStorage.setItem('payfrit_portal_business', biz.BusinessID);
window.location.reload();
};
list.appendChild(item);
});
content.querySelector('#closeChooser').onclick = () => modal.remove();
modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
modal.appendChild(content);
document.body.appendChild(modal);
}, },
// Add a new business (go to setup wizard) // Add a new business (go to setup wizard)

View file

@ -718,7 +718,7 @@
<div class="business-status online">Online</div> <div class="business-status online">Online</div>
</div> </div>
</div> </div>
<a href="signup.html" class="nav-item logout" onclick="logout()"> <a href="login.html" class="nav-item logout" onclick="logout()">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16,17 21,12 16,7"/> <polyline points="16,17 21,12 16,7"/>
@ -1077,7 +1077,7 @@
// Check if user is logged in // Check if user is logged in
const userId = localStorage.getItem('payfrit_portal_userid'); const userId = localStorage.getItem('payfrit_portal_userid');
if (!userId) { if (!userId) {
window.location.href = 'signup.html'; window.location.href = 'login.html';
return; return;
} }
config.userId = userId; config.userId = userId;
@ -2525,7 +2525,7 @@
localStorage.removeItem('payfrit_portal_token'); localStorage.removeItem('payfrit_portal_token');
localStorage.removeItem('payfrit_portal_userid'); localStorage.removeItem('payfrit_portal_userid');
localStorage.removeItem('payfrit_portal_business'); localStorage.removeItem('payfrit_portal_business');
window.location.href = 'signup.html'; window.location.href = 'login.html';
} }
</script> </script>
</body> </body>

View file

@ -314,7 +314,8 @@
</form> </form>
<div class="signup-footer"> <div class="signup-footer">
<span style="color: var(--text-muted); font-size: 14px;">New or returning? Verify your phone to continue.</span> <span style="color: var(--text-muted); font-size: 14px;">Already have an account? </span>
<a href="login.html">Sign in with password</a>
</div> </div>
</div> </div>
@ -805,11 +806,8 @@
console.log('[Signup] myBusinesses response:', data); console.log('[Signup] myBusinesses response:', data);
if (data.OK && data.BUSINESSES && data.BUSINESSES.length > 0) { if (data.OK && data.BUSINESSES && data.BUSINESSES.length > 0) {
// Has businesses - go to portal (chooser will show if multiple) // Has businesses - go to login for business selection
if (data.BUSINESSES.length === 1) { window.location.href = BASE_PATH + '/portal/login.html';
localStorage.setItem('payfrit_portal_business', data.BUSINESSES[0].BusinessID);
}
window.location.href = BASE_PATH + '/portal/index.html';
} else { } else {
// No businesses - go to wizard // No businesses - go to wizard
this.redirectToWizard(); this.redirectToWizard();

View file

@ -437,7 +437,7 @@
const savedBusiness = localStorage.getItem('payfrit_portal_business'); const savedBusiness = localStorage.getItem('payfrit_portal_business');
if (!token || !savedBusiness) { if (!token || !savedBusiness) {
window.location.href = BASE_PATH + '/portal/signup.html'; window.location.href = BASE_PATH + '/portal/login.html';
return; return;
} }