Menu builder and portal updates
- Menu builder UI improvements - Portal CSS and JS updates - Station assignment updates - Add business tabs update endpoint Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e21a7f7266
commit
d7632c5d35
10 changed files with 326 additions and 182 deletions
|
|
@ -112,6 +112,7 @@ if (len(request._api_path)) {
|
||||||
if (findNoCase("/api/businesses/getChildren.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/businesses/getChildren.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/businesses/update.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/businesses/update.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/businesses/updateHours.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/businesses/updateHours.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/businesses/updateTabs.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/servicepoints/list.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/servicepoints/list.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/servicepoints/get.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/servicepoints/get.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/servicepoints/save.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/servicepoints/save.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,10 @@ try {
|
||||||
IsHiring,
|
IsHiring,
|
||||||
HeaderImageExtension,
|
HeaderImageExtension,
|
||||||
TaxRate,
|
TaxRate,
|
||||||
BrandColor
|
BrandColor,
|
||||||
|
SessionEnabled,
|
||||||
|
SessionLockMinutes,
|
||||||
|
SessionPaymentStrategy
|
||||||
FROM Businesses
|
FROM Businesses
|
||||||
WHERE ID = :businessID
|
WHERE ID = :businessID
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
@ -140,7 +143,10 @@ try {
|
||||||
"IsHiring": q.IsHiring == 1,
|
"IsHiring": q.IsHiring == 1,
|
||||||
"TaxRate": taxRate,
|
"TaxRate": taxRate,
|
||||||
"TaxRatePercent": taxRate * 100,
|
"TaxRatePercent": taxRate * 100,
|
||||||
"BrandColor": len(q.BrandColor) ? (left(q.BrandColor, 1) == chr(35) ? q.BrandColor : chr(35) & q.BrandColor) : ""
|
"BrandColor": len(q.BrandColor) ? (left(q.BrandColor, 1) == chr(35) ? q.BrandColor : chr(35) & q.BrandColor) : "",
|
||||||
|
"SessionEnabled": isNumeric(q.SessionEnabled) ? q.SessionEnabled : 0,
|
||||||
|
"SessionLockMinutes": isNumeric(q.SessionLockMinutes) ? q.SessionLockMinutes : 30,
|
||||||
|
"SessionPaymentStrategy": len(q.SessionPaymentStrategy) ? q.SessionPaymentStrategy : "A"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add header image URL if extension exists
|
// Add header image URL if extension exists
|
||||||
|
|
|
||||||
71
api/businesses/updateTabs.cfm
Normal file
71
api/businesses/updateTabs.cfm
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
function apiAbort(obj) {
|
||||||
|
writeOutput(serializeJSON(obj));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
raw = toString(getHttpRequestData().content);
|
||||||
|
if (isNull(raw) || len(trim(raw)) EQ 0) {
|
||||||
|
apiAbort({ OK = false, ERROR = "missing_body" });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
parsed = deserializeJSON(raw);
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({ OK = false, ERROR = "bad_json", MESSAGE = "Invalid JSON body" });
|
||||||
|
}
|
||||||
|
if (!isStruct(parsed)) {
|
||||||
|
apiAbort({ OK = false, ERROR = "bad_json", MESSAGE = "JSON must be an object" });
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = readJsonBody();
|
||||||
|
|
||||||
|
if (!structKeyExists(data, "BusinessID") || !isNumeric(data.BusinessID) || int(data.BusinessID) LTE 0) {
|
||||||
|
apiAbort({ OK = false, ERROR = "missing_BusinessID" });
|
||||||
|
}
|
||||||
|
|
||||||
|
BusinessID = int(data.BusinessID);
|
||||||
|
SessionEnabled = structKeyExists(data, "SessionEnabled") && isNumeric(data.SessionEnabled) ? int(data.SessionEnabled) : 0;
|
||||||
|
SessionLockMinutes = structKeyExists(data, "SessionLockMinutes") && isNumeric(data.SessionLockMinutes) ? int(data.SessionLockMinutes) : 30;
|
||||||
|
SessionPaymentStrategy = structKeyExists(data, "SessionPaymentStrategy") ? left(trim(data.SessionPaymentStrategy), 1) : "A";
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
if (SessionLockMinutes < 5) SessionLockMinutes = 5;
|
||||||
|
if (SessionLockMinutes > 480) SessionLockMinutes = 480;
|
||||||
|
if (SessionPaymentStrategy != "A" && SessionPaymentStrategy != "P") SessionPaymentStrategy = "A";
|
||||||
|
</cfscript>
|
||||||
|
|
||||||
|
<cftry>
|
||||||
|
<cfquery datasource="payfrit">
|
||||||
|
UPDATE Businesses
|
||||||
|
SET SessionEnabled = <cfqueryparam cfsqltype="cf_sql_tinyint" value="#SessionEnabled#">,
|
||||||
|
SessionLockMinutes = <cfqueryparam cfsqltype="cf_sql_integer" value="#SessionLockMinutes#">,
|
||||||
|
SessionPaymentStrategy = <cfqueryparam cfsqltype="cf_sql_char" value="#SessionPaymentStrategy#">
|
||||||
|
WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#BusinessID#">
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<cfoutput>#serializeJSON({
|
||||||
|
"OK" = true,
|
||||||
|
"ERROR" = "",
|
||||||
|
"BusinessID" = BusinessID,
|
||||||
|
"SessionEnabled" = SessionEnabled,
|
||||||
|
"SessionLockMinutes" = SessionLockMinutes,
|
||||||
|
"SessionPaymentStrategy" = SessionPaymentStrategy
|
||||||
|
})#</cfoutput>
|
||||||
|
|
||||||
|
<cfcatch>
|
||||||
|
<cfoutput>#serializeJSON({
|
||||||
|
"OK" = false,
|
||||||
|
"ERROR" = "server_error",
|
||||||
|
"MESSAGE" = cfcatch.message
|
||||||
|
})#</cfoutput>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
|
@ -172,7 +172,7 @@ try {
|
||||||
// Get direct modifiers (items with ParentItemID pointing to menu items, not categories)
|
// Get direct modifiers (items with ParentItemID pointing to menu items, not categories)
|
||||||
qDirectModifiers = queryTimed("
|
qDirectModifiers = queryTimed("
|
||||||
SELECT
|
SELECT
|
||||||
m.ItemID,
|
m.ID as ItemID,
|
||||||
m.ParentItemID as ParentItemID,
|
m.ParentItemID as ParentItemID,
|
||||||
m.Name,
|
m.Name,
|
||||||
m.Price,
|
m.Price,
|
||||||
|
|
@ -192,16 +192,16 @@ try {
|
||||||
// NEW UNIFIED SCHEMA: Categories are Items at ParentID=0 with children
|
// NEW UNIFIED SCHEMA: Categories are Items at ParentID=0 with children
|
||||||
qCategories = queryTimed("
|
qCategories = queryTimed("
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
p.ItemID as CategoryID,
|
p.ID as CategoryID,
|
||||||
p.Name as Name,
|
p.Name as Name,
|
||||||
p.SortOrder
|
p.SortOrder
|
||||||
FROM Items p
|
FROM Items p
|
||||||
INNER JOIN Items c ON c.ParentItemID = p.ItemID
|
INNER JOIN Items c ON c.ParentItemID = p.ID
|
||||||
WHERE p.BusinessID = :businessID
|
WHERE p.BusinessID = :businessID
|
||||||
AND p.ParentItemID = 0
|
AND p.ParentItemID = 0
|
||||||
AND p.IsActive = 1
|
AND p.IsActive = 1
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = p.ItemID
|
SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = p.ID
|
||||||
)
|
)
|
||||||
ORDER BY p.SortOrder, p.Name
|
ORDER BY p.SortOrder, p.Name
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
@ -228,7 +228,7 @@ try {
|
||||||
|
|
||||||
qDirectModifiers = queryTimed("
|
qDirectModifiers = queryTimed("
|
||||||
SELECT
|
SELECT
|
||||||
m.ItemID,
|
m.ID as ItemID,
|
||||||
m.ParentItemID as ParentItemID,
|
m.ParentItemID as ParentItemID,
|
||||||
m.Name,
|
m.Name,
|
||||||
m.Price,
|
m.Price,
|
||||||
|
|
@ -267,7 +267,7 @@ try {
|
||||||
// Get templates for this business only
|
// Get templates for this business only
|
||||||
qTemplates = queryTimed("
|
qTemplates = queryTimed("
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
t.ItemID,
|
t.ID as ItemID,
|
||||||
t.Name,
|
t.Name,
|
||||||
t.Price,
|
t.Price,
|
||||||
t.IsCheckedByDefault as IsDefault,
|
t.IsCheckedByDefault as IsDefault,
|
||||||
|
|
@ -292,7 +292,7 @@ try {
|
||||||
if (arrayLen(templateIds) > 0) {
|
if (arrayLen(templateIds) > 0) {
|
||||||
qTemplateChildren = queryTimed("
|
qTemplateChildren = queryTimed("
|
||||||
SELECT
|
SELECT
|
||||||
c.ItemID,
|
c.ID as ItemID,
|
||||||
c.ParentItemID as ParentItemID,
|
c.ParentItemID as ParentItemID,
|
||||||
c.Name,
|
c.Name,
|
||||||
c.Price,
|
c.Price,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ function saveOptionsRecursive(options, parentID, businessID) {
|
||||||
RequiresChildSelection = :requiresSelection,
|
RequiresChildSelection = :requiresSelection,
|
||||||
MaxNumSelectionReq = :maxSelections,
|
MaxNumSelectionReq = :maxSelections,
|
||||||
ParentItemID = :parentID
|
ParentItemID = :parentID
|
||||||
WHERE ItemID = :optID
|
WHERE ID = :optID
|
||||||
", {
|
", {
|
||||||
optID: optDbId,
|
optID: optDbId,
|
||||||
parentID: parentID,
|
parentID: parentID,
|
||||||
|
|
@ -122,7 +122,7 @@ try {
|
||||||
UPDATE Items
|
UPDATE Items
|
||||||
SET Name = :name,
|
SET Name = :name,
|
||||||
SortOrder = :sortOrder
|
SortOrder = :sortOrder
|
||||||
WHERE ItemID = :categoryID
|
WHERE ID = :categoryID
|
||||||
AND BusinessID = :businessID
|
AND BusinessID = :businessID
|
||||||
", {
|
", {
|
||||||
categoryID: categoryID,
|
categoryID: categoryID,
|
||||||
|
|
@ -203,7 +203,7 @@ try {
|
||||||
Price = :price,
|
Price = :price,
|
||||||
ParentItemID = :categoryID,
|
ParentItemID = :categoryID,
|
||||||
SortOrder = :sortOrder
|
SortOrder = :sortOrder
|
||||||
WHERE ItemID = :itemID
|
WHERE ID = :itemID
|
||||||
", {
|
", {
|
||||||
itemID: itemID,
|
itemID: itemID,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
|
|
@ -220,7 +220,7 @@ try {
|
||||||
Price = :price,
|
Price = :price,
|
||||||
CategoryID = :categoryID,
|
CategoryID = :categoryID,
|
||||||
SortOrder = :sortOrder
|
SortOrder = :sortOrder
|
||||||
WHERE ItemID = :itemID
|
WHERE ID = :itemID
|
||||||
", {
|
", {
|
||||||
itemID: itemID,
|
itemID: itemID,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
|
|
@ -299,7 +299,7 @@ try {
|
||||||
UPDATE Items
|
UPDATE Items
|
||||||
SET RequiresChildSelection = :requiresSelection,
|
SET RequiresChildSelection = :requiresSelection,
|
||||||
MaxNumSelectionReq = :maxSelections
|
MaxNumSelectionReq = :maxSelections
|
||||||
WHERE ItemID = :modID
|
WHERE ID = :modID
|
||||||
", {
|
", {
|
||||||
modID: modDbId,
|
modID: modDbId,
|
||||||
requiresSelection: requiresSelection,
|
requiresSelection: requiresSelection,
|
||||||
|
|
@ -324,7 +324,7 @@ try {
|
||||||
RequiresChildSelection = :requiresSelection,
|
RequiresChildSelection = :requiresSelection,
|
||||||
MaxNumSelectionReq = :maxSelections,
|
MaxNumSelectionReq = :maxSelections,
|
||||||
ParentItemID = :parentID
|
ParentItemID = :parentID
|
||||||
WHERE ItemID = :modID
|
WHERE ID = :modID
|
||||||
", {
|
", {
|
||||||
modID: modDbId,
|
modID: modDbId,
|
||||||
parentID: itemID,
|
parentID: itemID,
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
<cfset queryTimed("
|
<cfset queryTimed("
|
||||||
UPDATE Items
|
UPDATE Items
|
||||||
SET StationID = ?
|
SET StationID = ?
|
||||||
WHERE ItemID = ?
|
WHERE ID = ?
|
||||||
", [
|
", [
|
||||||
{ value = assignment.StationID, cfsqltype = "cf_sql_integer" },
|
{ value = assignment.StationID, cfsqltype = "cf_sql_integer" },
|
||||||
{ value = assignment.ItemID, cfsqltype = "cf_sql_integer" }
|
{ value = assignment.ItemID, cfsqltype = "cf_sql_integer" }
|
||||||
|
|
|
||||||
|
|
@ -19,27 +19,20 @@
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<aside class="sidebar" id="sidebar">
|
<aside class="sidebar" id="sidebar">
|
||||||
<div class="sidebar-header">
|
<!-- Business Card -->
|
||||||
<div class="logo">
|
<div class="business-info" style="padding: 12px 16px; border-bottom: 1px solid var(--gray-200); display: flex; align-items: center; gap: 12px;">
|
||||||
<span class="logo-icon">P</span>
|
<div class="business-avatar" id="businessAvatar">B</div>
|
||||||
<span class="logo-text">Payfrit</span>
|
<div class="business-details" style="flex: 1;">
|
||||||
|
<div class="business-name" id="businessName">Loading...</div>
|
||||||
|
<div class="business-status online">Online</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="sidebar-toggle" id="sidebarToggle">
|
<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">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M3 12h18M3 6h18M3 18h18"/>
|
<path d="M3 12h18M3 6h18M3 18h18"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Business Card -->
|
|
||||||
<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-details">
|
|
||||||
<div class="business-name" id="businessName">Loading...</div>
|
|
||||||
<div class="business-status online">Online</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<a href="#dashboard" class="nav-item active" data-page="dashboard">
|
<a href="#dashboard" class="nav-item active" data-page="dashboard">
|
||||||
<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">
|
||||||
|
|
@ -76,14 +69,6 @@
|
||||||
</svg>
|
</svg>
|
||||||
<span>Team</span>
|
<span>Team</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#beacons" class="nav-item" data-page="beacons">
|
|
||||||
<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>Beacons</span>
|
|
||||||
</a>
|
|
||||||
<a href="#services" class="nav-item" data-page="services">
|
<a href="#services" class="nav-item" data-page="services">
|
||||||
<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="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
||||||
|
|
@ -91,11 +76,13 @@
|
||||||
</svg>
|
</svg>
|
||||||
<span>Services</span>
|
<span>Services</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#sp-sharing" class="nav-item" data-page="sp-sharing">
|
<a href="#service-points" class="nav-item" data-page="service-points">
|
||||||
<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="M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/>
|
<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>
|
</svg>
|
||||||
<span>SP Sharing</span>
|
<span>Service Points</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#admin-tasks" class="nav-item" data-page="admin-tasks">
|
<a href="#admin-tasks" class="nav-item" data-page="admin-tasks">
|
||||||
<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">
|
||||||
|
|
@ -397,23 +384,10 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Beacons Page -->
|
<!-- Service Points Page (combined: Service Points, Beacons, Sharing) -->
|
||||||
<section class="page" id="page-beacons">
|
<section class="page" id="page-service-points">
|
||||||
<div class="beacons-layout">
|
<div class="service-points-layout">
|
||||||
<!-- Left side: Beacons list -->
|
<!-- Service Points list -->
|
||||||
<div class="card">
|
|
||||||
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
|
||||||
<h3>Beacons</h3>
|
|
||||||
<button class="btn btn-sm btn-primary" onclick="Portal.showBeaconModal()">+ Add Beacon</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="list-group" id="beaconsList">
|
|
||||||
<div class="empty-state">Loading beacons...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Middle: Service Points list -->
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||||
<h3>Service Points</h3>
|
<h3>Service Points</h3>
|
||||||
|
|
@ -426,7 +400,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right side: Assignments -->
|
<!-- Beacons list -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||||
|
<h3>Beacons</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="Portal.showBeaconModal()">+ Add Beacon</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="list-group" id="beaconsList">
|
||||||
|
<div class="empty-state">Loading beacons...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Beacon Assignments -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||||
<h3>Beacon Assignments</h3>
|
<h3>Beacon Assignments</h3>
|
||||||
|
|
@ -438,6 +425,82 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SP Sharing: Grants You've Created -->
|
||||||
|
<div class="card" id="spSharingOwnerCard">
|
||||||
|
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<h3>Grants You've Created</h3>
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="Portal.showInviteBusinessModal()">Invite Business</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="ownerGrantsList">
|
||||||
|
<div class="loading-text">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SP Sharing: Invites & Active Grants -->
|
||||||
|
<div class="card" id="spSharingGuestCard">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Invites & Active Grants</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="guestGrantsList">
|
||||||
|
<div class="loading-text">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Invite Business Modal -->
|
||||||
|
<div class="modal-overlay" id="inviteBusinessModal" style="display:none">
|
||||||
|
<div class="modal" style="max-width:500px">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Invite Business to Service Point</h3>
|
||||||
|
<button class="modal-close" onclick="Portal.closeInviteModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Search Business</label>
|
||||||
|
<input type="text" id="inviteBizSearch" class="form-input" placeholder="Business name or ID..." oninput="Portal.searchBusinessForInvite()">
|
||||||
|
<div id="inviteBizResults" style="margin-top:8px"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="inviteBizSelected" style="display:none">
|
||||||
|
<label>Selected Business</label>
|
||||||
|
<div id="inviteBizSelectedName" class="form-input" style="background:var(--bg-secondary)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Service Point</label>
|
||||||
|
<select id="inviteSPSelect" class="form-input"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Economics</label>
|
||||||
|
<select id="inviteEconType" class="form-input" onchange="Portal.toggleEconValue()">
|
||||||
|
<option value="none">None</option>
|
||||||
|
<option value="flat_fee">Flat Fee ($/order)</option>
|
||||||
|
<option value="percent_of_orders">Percentage of Orders</option>
|
||||||
|
</select>
|
||||||
|
<input type="number" id="inviteEconValue" class="form-input" placeholder="0" step="0.01" min="0" style="display:none;margin-top:4px">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Eligibility</label>
|
||||||
|
<select id="inviteEligibility" class="form-input">
|
||||||
|
<option value="public">Public (anyone)</option>
|
||||||
|
<option value="employees">Employees only</option>
|
||||||
|
<option value="guests">Guests only</option>
|
||||||
|
<option value="internal">Internal (owner's employees)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Time Policy</label>
|
||||||
|
<select id="inviteTimePolicy" class="form-input">
|
||||||
|
<option value="always">Always</option>
|
||||||
|
<option value="schedule">Schedule</option>
|
||||||
|
<option value="date_range">Date Range</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn" onclick="Portal.closeInviteModal()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="Portal.submitGrantInvite()">Send Invite</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -519,87 +582,6 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- SP Sharing Page -->
|
|
||||||
<section class="page" id="page-sp-sharing">
|
|
||||||
<div class="sp-sharing-container">
|
|
||||||
<!-- Owner Section -->
|
|
||||||
<div class="card" id="spSharingOwnerCard">
|
|
||||||
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
|
|
||||||
<h3>Grants You've Created</h3>
|
|
||||||
<button class="btn btn-primary btn-sm" onclick="Portal.showInviteBusinessModal()">Invite Business</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" id="ownerGrantsList">
|
|
||||||
<div class="loading-text">Loading...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Guest Section -->
|
|
||||||
<div class="card" id="spSharingGuestCard">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3>Invites & Active Grants</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" id="guestGrantsList">
|
|
||||||
<div class="loading-text">Loading...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Invite Business Modal -->
|
|
||||||
<div class="modal-overlay" id="inviteBusinessModal" style="display:none">
|
|
||||||
<div class="modal" style="max-width:500px">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>Invite Business to Service Point</h3>
|
|
||||||
<button class="modal-close" onclick="Portal.closeInviteModal()">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Search Business</label>
|
|
||||||
<input type="text" id="inviteBizSearch" class="form-input" placeholder="Business name or ID..." oninput="Portal.searchBusinessForInvite()">
|
|
||||||
<div id="inviteBizResults" style="margin-top:8px"></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" id="inviteBizSelected" style="display:none">
|
|
||||||
<label>Selected Business</label>
|
|
||||||
<div id="inviteBizSelectedName" class="form-input" style="background:var(--bg-secondary)"></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Service Point</label>
|
|
||||||
<select id="inviteSPSelect" class="form-input"></select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Economics</label>
|
|
||||||
<select id="inviteEconType" class="form-input" onchange="Portal.toggleEconValue()">
|
|
||||||
<option value="none">None</option>
|
|
||||||
<option value="flat_fee">Flat Fee ($/order)</option>
|
|
||||||
<option value="percent_of_orders">Percentage of Orders</option>
|
|
||||||
</select>
|
|
||||||
<input type="number" id="inviteEconValue" class="form-input" placeholder="0" step="0.01" min="0" style="display:none;margin-top:4px">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Eligibility</label>
|
|
||||||
<select id="inviteEligibility" class="form-input">
|
|
||||||
<option value="public">Public (anyone)</option>
|
|
||||||
<option value="employees">Employees only</option>
|
|
||||||
<option value="guests">Guests only</option>
|
|
||||||
<option value="internal">Internal (owner's employees)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Time Policy</label>
|
|
||||||
<select id="inviteTimePolicy" class="form-input">
|
|
||||||
<option value="always">Always</option>
|
|
||||||
<option value="schedule">Schedule</option>
|
|
||||||
<option value="date_range">Date Range</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn" onclick="Portal.closeInviteModal()">Cancel</button>
|
|
||||||
<button class="btn btn-primary" onclick="Portal.submitGrantInvite()">Send Invite</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Settings Page -->
|
<!-- Settings Page -->
|
||||||
<section class="page" id="page-settings">
|
<section class="page" id="page-settings">
|
||||||
<div class="settings-grid">
|
<div class="settings-grid">
|
||||||
|
|
@ -713,6 +695,37 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Tabs / Running Checks</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p style="color: #666; font-size: 13px; margin-bottom: 16px;">Allow customers to keep a tab open and order multiple rounds before closing out.</p>
|
||||||
|
<div class="form-group" style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="tabsEnabled" onchange="Portal.saveTabSettings()">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<span>Enable Tabs</span>
|
||||||
|
</div>
|
||||||
|
<div id="tabSettingsDetails" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tab Lock Duration (minutes)</label>
|
||||||
|
<input type="number" id="tabLockMinutes" class="form-input" value="30" min="5" max="480" style="width: 120px;">
|
||||||
|
<small style="color:#666;font-size:12px;display:block;margin-top:4px;">How long a tab stays open without activity</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top: 12px;">
|
||||||
|
<label>Payment Strategy</label>
|
||||||
|
<select id="tabPaymentStrategy" class="form-input" style="width: 200px;">
|
||||||
|
<option value="A">Pay at end (single charge)</option>
|
||||||
|
<option value="P">Pre-authorize card</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick="Portal.saveTabSettings()" style="margin-top: 12px;">Save Tab Settings</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Customer Preview</h3>
|
<h3>Customer Preview</h3>
|
||||||
|
|
@ -749,7 +762,7 @@
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
<div class="toast-container" id="toastContainer"></div>
|
<div class="toast-container" id="toastContainer"></div>
|
||||||
|
|
||||||
<script src="portal.js?v=9"></script>
|
<script src="portal.js?v=13"></script>
|
||||||
|
|
||||||
<!-- Close dropdown when clicking outside -->
|
<!-- Close dropdown when clicking outside -->
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
|
|
@ -763,20 +763,8 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<!-- Sidebar (same as portal) -->
|
<!-- Sidebar (without Payfrit header) -->
|
||||||
<aside class="sidebar" id="sidebar">
|
<aside class="sidebar" id="sidebar">
|
||||||
<div class="sidebar-header">
|
|
||||||
<div class="logo">
|
|
||||||
<span class="logo-icon">P</span>
|
|
||||||
<span class="logo-text">Payfrit</span>
|
|
||||||
</div>
|
|
||||||
<button class="sidebar-toggle" id="sidebarToggle">
|
|
||||||
<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 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);">
|
||||||
<div class="business-avatar" id="businessAvatar">B</div>
|
<div class="business-avatar" id="businessAvatar">B</div>
|
||||||
<div class="business-details">
|
<div class="business-details">
|
||||||
|
|
@ -821,14 +809,6 @@
|
||||||
</svg>
|
</svg>
|
||||||
<span>Team</span>
|
<span>Team</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="index.html#beacons" 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>Beacons</span>
|
|
||||||
</a>
|
|
||||||
<a href="index.html#services" class="nav-item">
|
<a href="index.html#services" 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="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
||||||
|
|
|
||||||
|
|
@ -587,7 +587,7 @@ body {
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Beacons Page Layout */
|
/* Beacons Page Layout (legacy) */
|
||||||
.beacons-layout {
|
.beacons-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
|
@ -600,6 +600,19 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Service Points Page Layout */
|
||||||
|
.service-points-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.service-points-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.list-group {
|
.list-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -974,6 +987,14 @@ body {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid var(--gray-200);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Toast */
|
/* Toast */
|
||||||
.toast-container {
|
.toast-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
|
||||||
|
|
@ -215,10 +215,9 @@ const Portal = {
|
||||||
menu: 'Menu Management',
|
menu: 'Menu Management',
|
||||||
reports: 'Reports',
|
reports: 'Reports',
|
||||||
team: 'Team',
|
team: 'Team',
|
||||||
beacons: 'Beacons',
|
|
||||||
services: 'Service Requests',
|
services: 'Service Requests',
|
||||||
|
'service-points': 'Service Points',
|
||||||
'admin-tasks': 'Task Admin',
|
'admin-tasks': 'Task Admin',
|
||||||
'sp-sharing': 'SP Sharing',
|
|
||||||
settings: 'Settings'
|
settings: 'Settings'
|
||||||
};
|
};
|
||||||
document.getElementById('pageTitle').textContent = titles[page] || page;
|
document.getElementById('pageTitle').textContent = titles[page] || page;
|
||||||
|
|
@ -250,18 +249,15 @@ const Portal = {
|
||||||
case 'team':
|
case 'team':
|
||||||
await this.loadTeam();
|
await this.loadTeam();
|
||||||
break;
|
break;
|
||||||
case 'beacons':
|
|
||||||
await this.loadBeaconsPage();
|
|
||||||
break;
|
|
||||||
case 'services':
|
case 'services':
|
||||||
await this.loadServicesPage();
|
await this.loadServicesPage();
|
||||||
break;
|
break;
|
||||||
|
case 'service-points':
|
||||||
|
await this.loadServicePointsPage();
|
||||||
|
break;
|
||||||
case 'admin-tasks':
|
case 'admin-tasks':
|
||||||
await this.loadAdminTasksPage();
|
await this.loadAdminTasksPage();
|
||||||
break;
|
break;
|
||||||
case 'sp-sharing':
|
|
||||||
await this.loadSPSharingPage();
|
|
||||||
break;
|
|
||||||
case 'settings':
|
case 'settings':
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
break;
|
break;
|
||||||
|
|
@ -732,10 +728,10 @@ const Portal = {
|
||||||
this.currentBusiness = biz;
|
this.currentBusiness = biz;
|
||||||
|
|
||||||
// Populate form fields (Lucee serializes all keys as uppercase)
|
// Populate form fields (Lucee serializes all keys as uppercase)
|
||||||
document.getElementById('settingName').value = biz.BUSINESSNAME || biz.Name || '';
|
document.getElementById('settingBusinessName').value = biz.BUSINESSNAME || biz.Name || '';
|
||||||
document.getElementById('settingPhone').value = biz.BUSINESSPHONE || biz.Phone || '';
|
document.getElementById('settingPhone').value = biz.BUSINESSPHONE || biz.Phone || '';
|
||||||
document.getElementById('settingTaxRate').value = biz.TAXRATEPERCENT || biz.TaxRatePercent || '';
|
document.getElementById('settingTaxRate').value = biz.TAXRATEPERCENT || biz.TaxRatePercent || '';
|
||||||
document.getElementById('settingLine1').value = biz.ADDRESSLINE1 || biz.Line1 || '';
|
document.getElementById('settingAddressLine1').value = biz.ADDRESSLINE1 || biz.Line1 || '';
|
||||||
document.getElementById('settingCity').value = biz.ADDRESSCITY || biz.City || '';
|
document.getElementById('settingCity').value = biz.ADDRESSCITY || biz.City || '';
|
||||||
document.getElementById('settingState').value = biz.ADDRESSSTATE || biz.AddressState || '';
|
document.getElementById('settingState').value = biz.ADDRESSSTATE || biz.AddressState || '';
|
||||||
document.getElementById('settingZip').value = biz.ADDRESSZIP || biz.AddressZip || '';
|
document.getElementById('settingZip').value = biz.ADDRESSZIP || biz.AddressZip || '';
|
||||||
|
|
@ -757,12 +753,62 @@ const Portal = {
|
||||||
|
|
||||||
// Render hours editor
|
// Render hours editor
|
||||||
this.renderHoursEditor(biz.BUSINESSHOURSDETAIL || biz.HoursDetail || []);
|
this.renderHoursEditor(biz.BUSINESSHOURSDETAIL || biz.HoursDetail || []);
|
||||||
|
|
||||||
|
// Load tab settings
|
||||||
|
const tabsEnabled = biz.SESSIONENABLED || biz.SessionEnabled || 0;
|
||||||
|
const tabLockMinutes = biz.SESSIONLOCKMINUTES || biz.SessionLockMinutes || 30;
|
||||||
|
const tabPaymentStrategy = biz.SESSIONPAYMENTSTRATEGY || biz.SessionPaymentStrategy || 'A';
|
||||||
|
|
||||||
|
const tabsCheckbox = document.getElementById('tabsEnabled');
|
||||||
|
const tabDetails = document.getElementById('tabSettingsDetails');
|
||||||
|
if (tabsCheckbox) {
|
||||||
|
tabsCheckbox.checked = tabsEnabled == 1;
|
||||||
|
if (tabDetails) tabDetails.style.display = tabsEnabled == 1 ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
const lockInput = document.getElementById('tabLockMinutes');
|
||||||
|
if (lockInput) lockInput.value = tabLockMinutes;
|
||||||
|
const strategySelect = document.getElementById('tabPaymentStrategy');
|
||||||
|
if (strategySelect) strategySelect.value = tabPaymentStrategy;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Portal] Error loading business info:', err);
|
console.error('[Portal] Error loading business info:', err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Save tab settings
|
||||||
|
async saveTabSettings() {
|
||||||
|
const tabsEnabled = document.getElementById('tabsEnabled').checked ? 1 : 0;
|
||||||
|
const tabLockMinutes = parseInt(document.getElementById('tabLockMinutes').value) || 30;
|
||||||
|
const tabPaymentStrategy = document.getElementById('tabPaymentStrategy').value || 'A';
|
||||||
|
|
||||||
|
// Show/hide details based on toggle
|
||||||
|
const tabDetails = document.getElementById('tabSettingsDetails');
|
||||||
|
if (tabDetails) tabDetails.style.display = tabsEnabled ? 'block' : 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/businesses/updateTabs.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
BusinessID: this.config.businessId,
|
||||||
|
SessionEnabled: tabsEnabled,
|
||||||
|
SessionLockMinutes: tabLockMinutes,
|
||||||
|
SessionPaymentStrategy: tabPaymentStrategy
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.showToast('Tab settings saved!', 'success');
|
||||||
|
} else {
|
||||||
|
this.showToast(data.ERROR || 'Failed to save tab settings', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Portal] Error saving tab settings:', err);
|
||||||
|
this.showToast('Error saving tab settings', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Render hours editor
|
// Render hours editor
|
||||||
renderHoursEditor(hours) {
|
renderHoursEditor(hours) {
|
||||||
const container = document.getElementById('hoursEditor');
|
const container = document.getElementById('hoursEditor');
|
||||||
|
|
@ -845,10 +891,10 @@ const Portal = {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
BusinessID: this.config.businessId,
|
BusinessID: this.config.businessId,
|
||||||
Name: document.getElementById('settingName').value,
|
Name: document.getElementById('settingBusinessName').value,
|
||||||
Phone: document.getElementById('settingPhone').value,
|
Phone: document.getElementById('settingPhone').value,
|
||||||
TaxRatePercent: parseFloat(document.getElementById('settingTaxRate').value) || 0,
|
TaxRatePercent: parseFloat(document.getElementById('settingTaxRate').value) || 0,
|
||||||
Line1: document.getElementById('settingLine1').value,
|
Line1: document.getElementById('settingAddressLine1').value,
|
||||||
City: document.getElementById('settingCity').value,
|
City: document.getElementById('settingCity').value,
|
||||||
State: document.getElementById('settingState').value,
|
State: document.getElementById('settingState').value,
|
||||||
Zip: document.getElementById('settingZip').value
|
Zip: document.getElementById('settingZip').value
|
||||||
|
|
@ -1540,14 +1586,20 @@ const Portal = {
|
||||||
assignments: [],
|
assignments: [],
|
||||||
|
|
||||||
// Load beacons page data
|
// Load beacons page data
|
||||||
async loadBeaconsPage() {
|
async loadServicePointsPage() {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.loadBeacons(),
|
|
||||||
this.loadServicePoints(),
|
this.loadServicePoints(),
|
||||||
this.loadAssignments()
|
this.loadBeacons(),
|
||||||
|
this.loadAssignments(),
|
||||||
|
this.loadSPSharingPage()
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Legacy alias
|
||||||
|
async loadBeaconsPage() {
|
||||||
|
await this.loadServicePointsPage();
|
||||||
|
},
|
||||||
|
|
||||||
// Load beacons list
|
// Load beacons list
|
||||||
async loadBeacons() {
|
async loadBeacons() {
|
||||||
const container = document.getElementById('beaconsList');
|
const container = document.getElementById('beaconsList');
|
||||||
|
|
@ -1681,11 +1733,11 @@ const Portal = {
|
||||||
container.innerHTML = this.assignments.map(a => `
|
container.innerHTML = this.assignments.map(a => `
|
||||||
<div class="list-group-item">
|
<div class="list-group-item">
|
||||||
<div class="item-info">
|
<div class="item-info">
|
||||||
<div class="item-name">${this.escapeHtml(a.Name)} → ${this.escapeHtml(a.Name)}</div>
|
<div class="item-name">${this.escapeHtml(a.BeaconName)} → ${this.escapeHtml(a.ServicePointName)}</div>
|
||||||
<div class="item-detail">${a.lt_Beacon_Businesses_ServicePointNotes || ''}</div>
|
<div class="item-detail">UUID: ${a.UUID || ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-actions">
|
<div class="item-actions">
|
||||||
<button class="btn btn-sm btn-danger" onclick="Portal.deleteAssignment(${a.lt_Beacon_Businesses_ServicePointID})">Remove</button>
|
<button class="btn btn-sm btn-danger" onclick="Portal.deleteAssignment(${a.ServicePointID})">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
@ -1988,7 +2040,7 @@ const Portal = {
|
||||||
'X-User-Token': this.config.token,
|
'X-User-Token': this.config.token,
|
||||||
'X-Business-ID': this.config.businessId
|
'X-Business-ID': this.config.businessId
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ lt_Beacon_Businesses_ServicePointID: assignmentId })
|
body: JSON.stringify({ ServicePointID: assignmentId })
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
|
|
||||||
Reference in a new issue