Resolve merge conflict in myBusinesses.cfm - keep ActiveTaskCount
This commit is contained in:
parent
e4422996b2
commit
0d04ae8463
18 changed files with 4674 additions and 5327 deletions
|
|
@ -108,12 +108,15 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
try{logPerf(0);}catch(any e){}
|
try{logPerf(0);}catch(any e){}
|
||||||
writeOutput(serializeJSON({
|
resp = {
|
||||||
"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({
|
||||||
|
|
|
||||||
|
|
@ -150,12 +150,15 @@ try {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOutput(serializeJSON({
|
resp = {
|
||||||
"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({
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
// 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 };
|
response = { "OK": false, "VERSION": "2026-02-08-fix2", "DEBUG": [] };
|
||||||
|
|
||||||
// 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 = {};
|
||||||
|
|
@ -95,18 +96,21 @@ try {
|
||||||
throw("Menu categories are required");
|
throw("Menu categories are required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if new schema is active
|
// Check if Categories table has data (must match getForBuilder logic!)
|
||||||
newSchemaActive = false;
|
// If Categories table has data, use legacy schema (CategoryID in Items)
|
||||||
|
// Otherwise use unified schema (ParentItemID in Items)
|
||||||
|
hasCategoriesData = false;
|
||||||
try {
|
try {
|
||||||
qCheck = queryTimed("
|
qCatCheck = queryTimed("
|
||||||
SELECT 1 FROM Items
|
SELECT 1 FROM Categories
|
||||||
WHERE BusinessID = :businessID AND BusinessID > 0
|
WHERE BusinessID = :businessID
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
", { businessID: businessID });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
newSchemaActive = (qCheck.recordCount > 0);
|
hasCategoriesData = (qCatCheck.recordCount > 0);
|
||||||
} catch (any e) {
|
} catch (any e) {
|
||||||
newSchemaActive = false;
|
hasCategoriesData = false;
|
||||||
}
|
}
|
||||||
|
newSchemaActive = !hasCategoriesData;
|
||||||
|
|
||||||
// Wrap everything in a transaction for speed and consistency
|
// Wrap everything in a transaction for speed and consistency
|
||||||
transaction {
|
transaction {
|
||||||
|
|
@ -114,6 +118,13 @@ 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) {
|
||||||
|
|
@ -151,31 +162,27 @@ 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 = :menuId
|
MenuID = NULLIF(:menuId, 0)
|
||||||
WHERE CategoryID = :categoryID
|
WHERE ID = :categoryID
|
||||||
", {
|
", {
|
||||||
categoryID: categoryID,
|
categoryID: categoryID,
|
||||||
name: cat.name,
|
name: cat.name,
|
||||||
sortOrder: catSortOrder,
|
sortOrder: catSortOrder,
|
||||||
menuId: categoryMenuIdParam
|
menuId: categoryMenuId
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
queryTimed("
|
queryTimed("
|
||||||
INSERT INTO Categories (BusinessID, MenuID, Name, SortOrder, AddedOn)
|
INSERT INTO Categories (BusinessID, MenuID, Name, SortOrder, AddedOn)
|
||||||
VALUES (:businessID, :menuId, :name, :sortOrder, NOW())
|
VALUES (:businessID, NULLIF(:menuId, 0), :name, :sortOrder, NOW())
|
||||||
", {
|
", {
|
||||||
businessID: businessID,
|
businessID: businessID,
|
||||||
menuId: categoryMenuIdParam,
|
menuId: categoryMenuId,
|
||||||
name: cat.name,
|
name: cat.name,
|
||||||
sortOrder: catSortOrder
|
sortOrder: catSortOrder
|
||||||
});
|
});
|
||||||
|
|
@ -185,6 +192,9 @@ 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;
|
||||||
|
|
@ -192,6 +202,9 @@ 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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,13 @@
|
||||||
<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>
|
||||||
|
|
@ -35,20 +42,29 @@ 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 --->
|
||||||
<cfimage source="#headersDir#/#uploadResult.ServerFile#" action="info" structName="imageInfo">
|
|
||||||
|
|
||||||
<!--- Use the actual detected format, not the client-provided extension --->
|
|
||||||
<cfset actualFormat = lCase(imageInfo.source_file)>
|
|
||||||
<cfset actualExt = "">
|
<cfset actualExt = "">
|
||||||
<cfif findNoCase("jpeg", actualFormat) OR findNoCase("jpg", actualFormat)>
|
<cftry>
|
||||||
<cfset actualExt = "jpg">
|
<cfimage source="#headersDir#/#uploadResult.ServerFile#" action="info" structName="imageInfo">
|
||||||
<cfelseif findNoCase("png", actualFormat)>
|
<!--- Use the actual detected format from source_file if available --->
|
||||||
<cfset actualExt = "png">
|
<cfif structKeyExists(imageInfo, "source_file")>
|
||||||
<cfelseif findNoCase("gif", actualFormat)>
|
<cfset actualFormat = lCase(imageInfo.source_file)>
|
||||||
<cfset actualExt = "gif">
|
<cfif findNoCase("jpeg", actualFormat) OR findNoCase("jpg", actualFormat)>
|
||||||
<cfelseif findNoCase("webp", actualFormat)>
|
<cfset actualExt = "jpg">
|
||||||
<cfset actualExt = "webp">
|
<cfelseif findNoCase("png", actualFormat)>
|
||||||
<cfelse>
|
<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 --->
|
||||||
|
<cfif NOT len(actualExt)>
|
||||||
<!--- 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)>
|
||||||
|
|
@ -105,8 +121,15 @@ 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="#headersDir#/#uploadResult.ServerFile#" destination="#destFile#" mode="755">
|
<cffile action="RENAME" source="#sourceFile#" destination="#destFile#" mode="755">
|
||||||
|
|
||||||
<!--- Update database --->
|
<!--- Update database --->
|
||||||
<cfquery datasource="payfrit">
|
<cfquery datasource="payfrit">
|
||||||
|
|
@ -116,13 +139,15 @@ 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": imageInfo.width,
|
"WIDTH": imgWidth,
|
||||||
"HEIGHT": imageInfo.height
|
"HEIGHT": imgHeight
|
||||||
})#</cfoutput>
|
})#</cfoutput>
|
||||||
|
|
||||||
<cfcatch type="any">
|
<cfcatch type="any">
|
||||||
|
|
|
||||||
|
|
@ -122,16 +122,22 @@ 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);
|
||||||
imageWrite(thumb, "#itemsDir#/#itemId#_thumb.jpg", 0.85);
|
thumbPath = "#itemsDir#/#itemId#_thumb.jpg";
|
||||||
|
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);
|
||||||
imageWrite(medium, "#itemsDir#/#itemId#_medium.jpg", 0.85);
|
mediumPath = "#itemsDir#/#itemId#_medium.jpg";
|
||||||
|
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);
|
||||||
imageWrite(img, "#itemsDir#/#itemId#.jpg", 0.90);
|
fullPath = "#itemsDir#/#itemId#.jpg";
|
||||||
|
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) {}
|
||||||
|
|
|
||||||
|
|
@ -58,20 +58,14 @@ try {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get task type info if TaskTypeID provided (name + category)
|
// Get task type name if TaskTypeID provided
|
||||||
taskTypeName = "";
|
taskTypeName = "";
|
||||||
taskTypeCategoryID = 0;
|
|
||||||
if (taskTypeID > 0) {
|
if (taskTypeID > 0) {
|
||||||
typeQuery = queryExecute("
|
typeQuery = queryExecute("
|
||||||
SELECT Name, TaskCategoryID FROM tt_TaskTypes WHERE ID = :typeID
|
SELECT Name 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) {
|
if (typeQuery.recordCount && len(trim(typeQuery.Name))) {
|
||||||
if (len(trim(typeQuery.Name))) {
|
taskTypeName = typeQuery.Name;
|
||||||
taskTypeName = typeQuery.Name;
|
|
||||||
}
|
|
||||||
if (!isNull(typeQuery.TaskCategoryID) && isNumeric(typeQuery.TaskCategoryID) && typeQuery.TaskCategoryID > 0) {
|
|
||||||
taskTypeCategoryID = typeQuery.TaskCategoryID;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,40 +90,12 @@ try {
|
||||||
taskDetails &= "Customer is requesting assistance";
|
taskDetails &= "Customer is requesting assistance";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine category: use task type's category if set, otherwise fallback to "Service" category
|
// Insert task (no CategoryID - using TaskTypeID only)
|
||||||
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,
|
||||||
|
|
@ -138,8 +104,8 @@ try {
|
||||||
CreatedOn
|
CreatedOn
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:businessID,
|
:businessID,
|
||||||
|
:servicePointID,
|
||||||
:userID,
|
:userID,
|
||||||
:categoryID,
|
|
||||||
:orderID,
|
:orderID,
|
||||||
:taskTypeID,
|
:taskTypeID,
|
||||||
:title,
|
:title,
|
||||||
|
|
@ -149,8 +115,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" },
|
||||||
|
|
|
||||||
|
|
@ -29,55 +29,25 @@ try {
|
||||||
userID = val(jsonData.UserID ?: 0);
|
userID = val(jsonData.UserID ?: 0);
|
||||||
message = jsonData.Message ?: "";
|
message = jsonData.Message ?: "";
|
||||||
|
|
||||||
// Get task type info for display (including category)
|
// Get task type name for display
|
||||||
taskTypeQuery = queryTimed("
|
taskTypeQuery = queryTimed("
|
||||||
SELECT Name, Color, Icon, TaskCategoryID
|
SELECT Name FROM tt_TaskTypes WHERE ID = :taskTypeID
|
||||||
FROM tt_TaskTypes
|
|
||||||
WHERE ID = :taskTypeID
|
|
||||||
", { taskTypeID: taskTypeID }, { datasource: "payfrit" });
|
", { taskTypeID: taskTypeID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
taskTitle = message;
|
taskTitle = message;
|
||||||
categoryID = 0;
|
if (taskTypeQuery.recordCount && len(trim(taskTypeQuery.Name))) {
|
||||||
if (taskTypeQuery.recordCount) {
|
taskTitle = taskTypeQuery.Name;
|
||||||
if (len(trim(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 with ServicePointID and UserID
|
// Insert service bell task (no CategoryID - using TaskTypeID only)
|
||||||
queryTimed("
|
queryTimed("
|
||||||
INSERT INTO Tasks (
|
INSERT INTO Tasks (
|
||||||
BusinessID,
|
BusinessID,
|
||||||
ServicePointID,
|
ServicePointID,
|
||||||
TaskTypeID,
|
TaskTypeID,
|
||||||
CategoryID,
|
|
||||||
OrderID,
|
OrderID,
|
||||||
UserID,
|
UserID,
|
||||||
Title,
|
Title,
|
||||||
|
|
@ -88,7 +58,6 @@ try {
|
||||||
:businessID,
|
:businessID,
|
||||||
:servicePointID,
|
:servicePointID,
|
||||||
:taskTypeID,
|
:taskTypeID,
|
||||||
:categoryID,
|
|
||||||
:orderID,
|
:orderID,
|
||||||
:userID,
|
:userID,
|
||||||
:taskTitle,
|
:taskTitle,
|
||||||
|
|
@ -100,7 +69,6 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -34,20 +34,18 @@
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<cftry>
|
<cftry>
|
||||||
<!--- Get the task and linked order details --->
|
<!--- Get the task and linked order details (no categories - using task types only) --->
|
||||||
<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,
|
||||||
|
|
@ -68,7 +66,6 @@
|
||||||
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
|
||||||
|
|
@ -107,14 +104,12 @@
|
||||||
<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")) : "",
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,6 @@
|
||||||
SELECT
|
SELECT
|
||||||
t.ID,
|
t.ID,
|
||||||
t.BusinessID,
|
t.BusinessID,
|
||||||
t.CategoryID,
|
|
||||||
t.OrderID,
|
t.OrderID,
|
||||||
t.TaskTypeID,
|
t.TaskTypeID,
|
||||||
t.Title,
|
t.Title,
|
||||||
|
|
@ -78,8 +77,6 @@
|
||||||
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,
|
||||||
|
|
@ -89,7 +86,6 @@
|
||||||
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
|
||||||
|
|
@ -129,7 +125,6 @@
|
||||||
"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,
|
||||||
|
|
@ -139,8 +134,6 @@
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -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 CategoryID = val( structKeyExists(data,"CategoryID") ? data.CategoryID : 0 )>
|
<cfset TaskTypeID = val( structKeyExists(data,"TaskTypeID") ? data.TaskTypeID : 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 category if provided --->
|
<!--- Filter by task type if provided --->
|
||||||
<cfif CategoryID GT 0>
|
<cfif TaskTypeID GT 0>
|
||||||
<cfset arrayAppend(whereClauses, "t.CategoryID = ?")>
|
<cfset arrayAppend(whereClauses, "t.TaskTypeID = ?")>
|
||||||
<cfset arrayAppend(params, { value = CategoryID, cfsqltype = "cf_sql_integer" })>
|
<cfset arrayAppend(params, { value = TaskTypeID, cfsqltype = "cf_sql_integer" })>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<cfset whereSQL = arrayToList(whereClauses, " AND ")>
|
<cfset whereSQL = arrayToList(whereClauses, " AND ")>
|
||||||
|
|
@ -58,7 +58,6 @@
|
||||||
SELECT
|
SELECT
|
||||||
t.ID,
|
t.ID,
|
||||||
t.BusinessID,
|
t.BusinessID,
|
||||||
t.CategoryID,
|
|
||||||
t.OrderID,
|
t.OrderID,
|
||||||
t.TaskTypeID,
|
t.TaskTypeID,
|
||||||
t.Title,
|
t.Title,
|
||||||
|
|
@ -66,8 +65,6 @@
|
||||||
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,
|
||||||
|
|
@ -76,7 +73,6 @@
|
||||||
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)
|
||||||
|
|
@ -111,7 +107,6 @@
|
||||||
<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,
|
||||||
|
|
@ -119,8 +114,6 @@
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,14 @@
|
||||||
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" } ], { datasource = "payfrit" })>
|
", [ { value = UserID, cfsqltype = "cf_sql_integer" }, { value = UserID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
<cfset businesses = []>
|
<cfset businesses = []>
|
||||||
|
|
||||||
|
|
@ -59,7 +60,8 @@
|
||||||
"Address": "",
|
"Address": "",
|
||||||
"City": "",
|
"City": "",
|
||||||
"StatusID": qBusinesses.StatusID,
|
"StatusID": qBusinesses.StatusID,
|
||||||
"PendingTaskCount": qBusinesses.PendingTaskCount
|
"PendingTaskCount": qBusinesses.PendingTaskCount,
|
||||||
|
"ActiveTaskCount": qBusinesses.ActiveTaskCount
|
||||||
})>
|
})>
|
||||||
</cfloop>
|
</cfloop>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -225,13 +225,16 @@ 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.floor((now - created) / 1000);
|
return Math.max(0, 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')}`;
|
||||||
|
|
|
||||||
|
|
@ -4,772 +4,14 @@
|
||||||
<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>
|
||||||
<link rel="stylesheet" href="portal.css">
|
<script>
|
||||||
<style>
|
// Redirect to signup.html - single auth entry point
|
||||||
body {
|
// Existing users verified by phone get their existing account
|
||||||
background: var(--bg-primary);
|
// New users go through full setup flow
|
||||||
min-height: 100vh;
|
window.location.replace('signup.html');
|
||||||
display: flex;
|
</script>
|
||||||
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>
|
||||||
<div class="login-container">
|
<p>Redirecting...</p>
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -817,14 +817,19 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<!-- Sidebar (without Payfrit header) -->
|
<!-- Sidebar -->
|
||||||
<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);">
|
<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-avatar" id="businessAvatar">B</div>
|
<div class="business-avatar" id="businessAvatar">B</div>
|
||||||
<div class="business-details">
|
<div class="business-details" style="flex: 1;">
|
||||||
<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">
|
||||||
|
|
@ -870,6 +875,14 @@
|
||||||
</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"/>
|
||||||
|
|
@ -888,6 +901,37 @@
|
||||||
|
|
||||||
<!-- 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">
|
||||||
|
|
@ -1235,7 +1279,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/login.html';
|
window.location.href = BASE_PATH + '/portal/signup.html';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1279,7 +1323,18 @@
|
||||||
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/login.html';
|
window.location.href = BASE_PATH + '/portal/signup.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
|
||||||
|
|
@ -1293,11 +1348,15 @@
|
||||||
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 = biz.Name ? biz.Name.charAt(0).toUpperCase() : 'B';
|
if (businessAvatar) businessAvatar.textContent = initial;
|
||||||
|
// 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);
|
||||||
|
|
@ -1441,8 +1500,15 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('click', () => {
|
document.addEventListener('click', (e) => {
|
||||||
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';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
113
portal/portal.js
113
portal/portal.js
|
|
@ -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 || !savedBusiness) {
|
if (!token) {
|
||||||
// Not logged in - redirect to login
|
// No token - redirect to signup
|
||||||
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/login.html';
|
window.location.href = BASE_PATH + '/portal/signup.html';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use saved business ID from localStorage
|
// Use saved business ID from localStorage (might be null)
|
||||||
this.config.businessId = parseInt(savedBusiness) || null;
|
this.config.businessId = savedBusiness ? 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
|
// Verify user has access to this business (or get their businesses if none selected)
|
||||||
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,18 +82,30 @@ const Portal = {
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.OK && data.BUSINESSES) {
|
if (data.OK && data.BUSINESSES && data.BUSINESSES.length > 0) {
|
||||||
const hasAccess = data.BUSINESSES.some(b => b.BusinessID === this.config.businessId);
|
if (!this.config.businessId) {
|
||||||
if (!hasAccess && data.BUSINESSES.length > 0) {
|
// No business selected
|
||||||
// User doesn't have access to requested business, use their first business
|
if (data.BUSINESSES.length === 1) {
|
||||||
this.config.businessId = data.BUSINESSES[0].BusinessID;
|
// Only one business - auto-select it
|
||||||
localStorage.setItem('payfrit_portal_business', this.config.businessId);
|
this.config.businessId = data.BUSINESSES[0].BusinessID;
|
||||||
} else if (!hasAccess) {
|
localStorage.setItem('payfrit_portal_business', this.config.businessId);
|
||||||
// User has no businesses
|
} else {
|
||||||
this.toast('No businesses associated with your account', 'error');
|
// Multiple businesses - show chooser
|
||||||
this.logout();
|
this.showBusinessChooser(data.BUSINESSES);
|
||||||
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);
|
||||||
|
|
@ -133,14 +145,69 @@ 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/login.html';
|
window.location.href = BASE_PATH + '/portal/signup.html';
|
||||||
},
|
},
|
||||||
|
|
||||||
// Switch to a different business (go back to login to select)
|
// Switch to a different business - show chooser modal
|
||||||
switchBusiness() {
|
async switchBusiness() {
|
||||||
// Clear current business selection but keep token
|
const token = localStorage.getItem('payfrit_portal_token');
|
||||||
localStorage.removeItem('payfrit_portal_business');
|
const userId = localStorage.getItem('payfrit_portal_userid');
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -718,7 +718,7 @@
|
||||||
<div class="business-status online">Online</div>
|
<div class="business-status online">Online</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="login.html" class="nav-item logout" onclick="logout()">
|
<a href="signup.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 = 'login.html';
|
window.location.href = 'signup.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 = 'login.html';
|
window.location.href = 'signup.html';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -314,8 +314,7 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="signup-footer">
|
<div class="signup-footer">
|
||||||
<span style="color: var(--text-muted); font-size: 14px;">Already have an account? </span>
|
<span style="color: var(--text-muted); font-size: 14px;">New or returning? Verify your phone to continue.</span>
|
||||||
<a href="login.html">Sign in with password</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -806,8 +805,11 @@
|
||||||
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 login for business selection
|
// Has businesses - go to portal (chooser will show if multiple)
|
||||||
window.location.href = BASE_PATH + '/portal/login.html';
|
if (data.BUSINESSES.length === 1) {
|
||||||
|
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();
|
||||||
|
|
|
||||||
|
|
@ -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/login.html';
|
window.location.href = BASE_PATH + '/portal/signup.html';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Reference in a new issue