Compare commits

...

33 commits

Author SHA1 Message Date
John Mizerek
1210249f54 Normalize database column and table names across entire codebase
Update all SQL queries, query result references, and ColdFusion code to match
the renamed database schema. Tables use plural CamelCase, PKs are all `ID`,
column prefixes stripped (e.g. BusinessName→Name, UserFirstName→FirstName).

Key changes:
- Strip table-name prefixes from all column references (Businesses, Users,
  Addresses, Hours, Menus, Categories, Items, Stations, Orders,
  OrderLineItems, Tasks, TaskCategories, TaskRatings, QuickTaskTemplates,
  ScheduledTaskDefinitions, ChatMessages, Beacons, ServicePoints, Employees,
  VisitorTrackings, ApiPerfLogs, tt_States, tt_Days, tt_AddressTypes,
  tt_OrderTypes, tt_TaskTypes)
- Rename PK references from {TableName}ID to ID in all queries
- Rewrite 7 admin beacon files to use ServicePoints.BeaconID instead of
  dropped lt_Beacon_Businesses_ServicePoints link table
- Rewrite beacon assignment files (list, save, delete) for new schema
- Fix FK references incorrectly changed to ID (OrderLineItems.OrderID,
  Categories.MenuID, Tasks.CategoryID, ServicePoints.BeaconID)
- Update Addresses: AddressLat→Latitude, AddressLng→Longitude
- Update Users: UserPassword→Password, UserIsEmailVerified→IsEmailVerified,
  UserIsActive→IsActive, UserBalance→Balance, etc.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 15:39:12 -08:00
John Mizerek
dc9db32b58 Add API performance profiling, caching, and query optimizations
- Add queryTimed() wrapper and logPerf() for per-endpoint timing metrics
- Add api_perf_log table flush mechanism with background thread batching
- Add application-scope cache (appCacheGet/Put/Invalidate) with TTL
- Cache businesses/get (5m), addresses/states (24h), menu/items (2m)
- Fix N+1 queries in orders/history, orders/listForKDS (batch fetch)
- Fix correlated subquery in orders/getDetail (LEFT JOIN)
- Combine 4 queries into 1 in portal/stats (subselects)
- Optimize getForBuilder tree building with pre-indexed parent lookup
- Add cache invalidation in update, saveBrandColor, updateHours, saveFromBuilder
- New admin/perf.cfm dashboard (localhost-protected)
- Instrument top 10 endpoints with queryTimed + logPerf

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 20:41:27 -08:00
John Mizerek
a5fa1c1041 Skip empty categories in menu API response
Only include category headers that actually have items assigned
to them, preventing empty categories from showing up in the app.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 22:29:38 -08:00
John Mizerek
6b31b1abcf Remove time-based filtering from customer menu API
Always return all active menus in the response so chip selector
always appears. Show all items from all menus by default instead
of filtering by current time, which caused empty results outside
menu hours.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 22:18:10 -08:00
John Mizerek
3b48c3331d Add menu list and MenuID filter to items.cfm API
Returns active menus in response so mobile apps can show menu
selector chips. Accepts optional MenuID param to filter items
to a specific menu.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 20:54:37 -08:00
John Mizerek
9091461079 Auto-select active menu based on current time, allow manual override
When multiple menus exist, checks current time and day against menu
schedules. If exactly one menu matches, auto-selects it. If times
overlap or none match, shows all categories. User can still manually
select any menu or 'All Menus' from the dropdown.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 18:13:44 -08:00
John Mizerek
41ef1631ef Allow menu deletion when categories exist - unassign instead of block
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 18:01:51 -08:00
John Mizerek
3613776ff3 Fix undefined menuName in saveWizard when using provided menuId
The menuName variable was only defined in the else branch (new menu
creation) but referenced later in category logging. Now looks up the
menu name from DB when using a provided menuId.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 17:54:16 -08:00
John Mizerek
8086065d38 Fix MenuID key case: Lucee 7 preserves case, not MENUID
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 17:48:03 -08:00
John Mizerek
61c10d9175 Use sessionStorage instead of URL params for wizard menu context
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 17:36:46 -08:00
John Mizerek
ec6bfdd9c9 Fix menu save: javaCast null breaks variable access in Lucee 7
javaCast('null','') makes the variable truly undefined, causing
'variable doesn't exist' errors when referenced in query params.
Use empty string + len() check instead.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 17:33:21 -08:00
John Mizerek
2fc30b8568 Fix add-menu wizard: skip hours validation, show menu name, hide irrelevant sections
- Pass menuName in redirect URL from menu-builder
- Show menu name in wizard header and final review
- Skip hours validation in both JS and backend for add-menu mode
- Hide menu name, hours, and community meal fields in final step
- Add id to community meal card for toggling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 17:19:55 -08:00
John Mizerek
02a19f52be Add menu wizard flow: redirect to setup wizard after creating a new menu
When a new menu is created in Manage Menus, the user is now redirected
to the setup wizard with businessId and menuId params. The wizard skips
business info/header steps and goes straight to photo upload + category
extraction. Backend uses the provided menuId instead of creating a new
menu. Also removes temp debug from menus.cfm.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 17:08:31 -08:00
John Mizerek
65cf855ade Add menu/menus.cfm to public paths so menu save works
The endpoint was missing from the public paths whitelist in
Application.cfm, causing all requests to fail with not_logged_in.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 16:57:51 -08:00
John Mizerek
df64f161f3 Add debug tagcontext to menu save error response (temporary)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 16:56:00 -08:00
John Mizerek
6b4e5cc369 Add cfsqltype hints for nullable time params in menu save
Fixes save failure when MenuStartTime/MenuEndTime are null -
Lucee couldn't determine the SQL type without explicit hints.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 16:53:34 -08:00
John Mizerek
38b13b5bd9 Fix blank Manage Menus modal by setting title/body before showing
showMenuManager() was passing arguments to showModal() which accepts
none, so the modal content was silently discarded. Now follows the
same pattern as all other modal callers in the file.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 16:39:23 -08:00
John Mizerek
c6a45f7024 Fix CFML hash escaping in brand color migration script
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 15:17:46 -08:00
John Mizerek
b75eb8530a Fix header image spec: 1200x400 not 1220x400
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 15:16:42 -08:00
John Mizerek
7bba0fb511 Add one-time script to strip # from existing brand colors
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 15:11:01 -08:00
John Mizerek
cc96d891b2 Store brand color without # prefix, normalize on output
saveBrandColor.cfm no longer prepends # before storing. API responses
(get.cfm, items.cfm, getForBuilder.cfm) prepend # if missing so
consumers always get a CSS-ready value.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 14:58:58 -08:00
John Mizerek
8f9da2fbf0 Add Manage Menus toolbar button, photo upload, and various improvements
- Move menu manager button to toolbar next to Save Menu for visibility
- Implement server-side photo upload for menu items
- Strip base64 data URLs from save payload to reduce size
- Add scheduled tasks, quick tasks, ratings, and task categories APIs
- Add vertical support and brand color features

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 14:43:41 -08:00
John Mizerek
0d3c381ed6 Add about.cfm API endpoint for mobile app About screennAdds server-side content for About Payfrit screen allowing content updates without releasing new app versions.nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> 2026-01-28 00:38:58 -08:00
John Mizerek
400df7624f Simplify address API - delivery addresses only
- Remove TypeID/Label from list response
- Hardcode TypeID=2 (delivery) in add endpoint
- Filter to personal addresses only (BusinessID=0 or NULL)
- Just IsDefault flag, no home/work labels

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 00:12:18 -08:00
John Mizerek
6d5620d513 Filter out business addresses from user address list 2026-01-28 00:02:25 -08:00
John Mizerek
cadc66e46a Add address types endpoint, fix dev mode SMS skip
- Add /addresses/types.cfm - returns address types list
- Update /addresses/list.cfm - include TypeID in response
- Update /addresses/add.cfm - accept TypeID instead of hardcoded '2'
- Fix loginOTP.cfm and sendOTP.cfm to skip Twilio SMS on dev server

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 00:00:01 -08:00
John Mizerek
477cf6b8b5 Auto-login single-business users, add switch/add business links
- Skip business selection for users with only one business
- Add Switch Business and Add New Business links in portal sidebar
- Handle returning users switching businesses (token + no business)
2026-01-27 21:49:08 -08:00
John Mizerek
80aa65d7fa Fix populateBusinessSelect - remove continueBtn references
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 21:38:52 -08:00
John Mizerek
6f0229247f Fix login - remove orphaned businessForm event listener
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 21:38:08 -08:00
John Mizerek
51e979a679 Remove URL params - use localStorage for auth
- HUD reads businessId from localStorage instead of ?b= param
- Portal opens HUD/quick-tasks without URL params
- Business select auto-proceeds on selection (no button needed)
- Quick tasks reads from payfrit_portal_business localStorage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 21:32:47 -08:00
John Mizerek
f96d8f4fe3 Update HUD default business to 47
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 21:27:45 -08:00
John Mizerek
49ef916f34 Remove demo mode from HUD, fix task creation timezone
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 21:25:05 -08:00
John Mizerek
d4cc4b4a6c Fix service bell task creation for Android app
- Update create.cfm to handle TaskTypeID for service bell tasks
- Update hud.js to prefer TaskTypeColor/TaskTypeName for task display

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 20:52:59 -08:00
253 changed files with 11069 additions and 5519 deletions

View file

@ -24,9 +24,9 @@
<cfif request.UserID NEQ 0>
<cfquery name="check_user" datasource="#application.datasource#">
SELECT UserFirstName, UserBalance, UserImageExtension
SELECT FirstName, Balance, ImageExtension
FROM Users
WHERE UserID = #request.UserID#
WHERE ID = #request.UserID#
</cfquery>
</cfif>
@ -320,8 +320,8 @@
<cfif find("logout.cfm", request.cgiPath) EQ 0>
<cfif request.UserID NEQ 0>
<cfif check_user.UserImageExtension gt ""><img src="#application.image_display_prefix#users/thumbs/#request.UserID#.#check_user.UserImageExtension#" border="0" alt=""><br></cfif>
Hi, <cfif check_user.UserFirstName gt "">#check_user.UserFirstName#<br>#dollarformat(check_user.UserBalance)#<cfelse>Payfrit User</cfif><br>
<cfif check_user.ImageExtension gt ""><img src="#application.image_display_prefix#users/thumbs/#request.UserID#.#check_user.ImageExtension#" border="0" alt=""><br></cfif>
Hi, <cfif check_user.FirstName gt "">#check_user.FirstName#<br>#dollarformat(check_user.Balance)#<cfelse>Payfrit User</cfif><br>
<cfelse>
<form action="#application.wwwrootprefix#index.cfm" method="post" name="login_form" id="login_form">

5
CHANGELOG.md Normal file
View file

@ -0,0 +1,5 @@
# Payfrit Portal Updates
## Week of 2026-01-26
- Fixed saved cart not being detected when entering child business

View file

@ -116,13 +116,13 @@
<cfset cart_total = 0>
<CFQUERY name="get_queued_food" datasource="#application.datasource#" dbtype="ODBC">
SELECT A.CartID, A.AddedOn, A.Quantity, A.SpecialRemark, B.BusinessName, B.UserID, C.ItemName, A.Price, D.UserFirstName, D.LaerFirstName, D.Balance
SELECT A.CartID, A.AddedOn, A.Quantity, A.SpecialRemark, B.Name, B.UserID, C.Name, A.Price, D.FirstName, D.LaerFirstName, D.Balance
FROM dbo.Business_CartMaster A, dbo.BusinessMaster B, dbo.Business_ItemMaster C, Users D
WHERE A.UserID = D.UserID
AND
A.ItemID = C.ItemID
AND
B.BusinessID = C.BusinessID
B.ID = C.BusinessID
AND
C.BusinessID = #form.bizid#
AND
@ -170,11 +170,11 @@
</CFQUERY>
<CFQUERY name="get_last_inserted" datasource="#application.datasource#" dbtype="ODBC">
SELECT TOP 1 O.OrderID, M.UserID as person_to_pay_for_orderID, U.Balance
SELECT TOP 1 O.ID, M.UserID as person_to_pay_for_orderID, U.Balance
FROM dbo.Business_OrderMaster O, dbo.BusinessMaster M, Users U
WHERE O.BusinessID = M.BusinessID
AND
M.UserID = U.UserID
M.UserID = U.ID
ORDER BY O.AddedOn DESC
</CFQUERY>
@ -185,7 +185,7 @@
)
VALUES
(
#get_last_inserted.OrderID#,
#get_last_inserted.ID#,
#get_queued_food.CartID#
)
</CFQUERY>
@ -268,7 +268,7 @@
<CFQUERY name="get_user_104_balance" datasource="#application.datasource#" dbtype="ODBC">
SELECT balance
FROM Users
WHERE UserID = 104
WHERE ID = 104
</CFQUERY>
<CFQUERY name="transfer_fees_to_UserID_104" datasource="#application.datasource#" dbtype="ODBC">
@ -346,7 +346,7 @@
<CFQUERY name="get_user_104_balance" datasource="#application.datasource#" dbtype="ODBC">
SELECT balance
FROM Users
WHERE UserID = 104
WHERE ID = 104
</CFQUERY>
<CFQUERY name="transfer_fees_to_UserID_104" datasource="#application.datasource#" dbtype="ODBC">

View file

@ -164,8 +164,8 @@ async function refreshAssignments(){
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${a.lt_Beacon_Businesses_ServicePointID}</td>
<td>${escapeHtml((a.BeaconName || "") + " (ID " + a.BeaconID + ")")}</td>
<td>${escapeHtml((a.ServicePointName || "") + " (ID " + a.ServicePointID + ")")}</td>
<td>${escapeHtml((a.Name || "") + " (ID " + a.BeaconID + ")")}</td>
<td>${escapeHtml((a.Name || "") + " (ID " + a.ServicePointID + ")")}</td>
<td>${escapeHtml(a.lt_Beacon_Businesses_ServicePointNotes || "")}</td>
<td>${escapeHtml(a.CreatedAt || "")}</td>
`;
@ -183,12 +183,12 @@ async function refreshBeacons(assignedBeaconIDs, keepSelectedBeaconID){
setSelectPlaceholder(sel, "-- Select Beacon --");
(out.BEACONS || []).forEach(b => {
const isAssigned = assignedBeaconIDs.has(String(b.BeaconID));
const isAssigned = assignedBeaconIDs.has(String(b.ID));
if (HIDE_ASSIGNED_BEACONS && isAssigned) return;
const opt = document.createElement("option");
opt.value = b.BeaconID;
opt.textContent = String(b.BeaconID) + " - " + (b.BeaconName || "");
opt.value = b.ID;
opt.textContent = String(b.ID) + " - " + (b.Name || "");
sel.appendChild(opt);
});
@ -203,12 +203,12 @@ async function refreshServicePoints(assignedServicePointIDs, keepSelectedService
setSelectPlaceholder(sel, "-- Select ServicePoint --");
(out.SERVICEPOINTS || []).forEach(sp => {
const isAssigned = assignedServicePointIDs.has(String(sp.ServicePointID));
const isAssigned = assignedServicePointIDs.has(String(sp.ID));
if (HIDE_ASSIGNED_SERVICEPOINTS && isAssigned) return;
const opt = document.createElement("option");
opt.value = sp.ServicePointID;
opt.textContent = String(sp.ServicePointID) + " - " + (sp.ServicePointName || "");
opt.value = sp.ID;
opt.textContent = String(sp.ID) + " - " + (sp.Name || "");
sel.appendChild(opt);
});

View file

@ -25,7 +25,7 @@
</head>
<body>
<h2>Beacons</h2>
<div class="warn">Required: BeaconName</div>
<div class="warn">Required: Name</div>
<div class="ok" id="jsStatus">(JS not loaded yet)</div>
<div class="row">
@ -38,8 +38,8 @@
</div>
<div>
<label>BeaconName (required)</label><br>
<input id="BeaconName" placeholder="Front Door" required>
<label>Name (required)</label><br>
<input id="Name" placeholder="Front Door" required>
</div>
<div>
@ -160,13 +160,13 @@ function escapeHtml(s){
}
function loadIntoForm(b){
document.getElementById("BeaconID").value = b.BeaconID || "";
document.getElementById("BeaconName").value = b.BeaconName || "";
document.getElementById("BeaconID").value = b.ID || "";
document.getElementById("Name").value = b.Name || "";
document.getElementById("UUID").value = b.UUID || "";
document.getElementById("NamespaceId").value = b.NamespaceId || "";
document.getElementById("InstanceId").value = b.InstanceId || "";
document.getElementById("IsActive").value = ("" + (b.IsActive ?? 1));
document.getElementById("DelBeaconID").value = b.BeaconID || "";
document.getElementById("DelBeaconID").value = b.ID || "";
}
async function refresh() {
@ -180,8 +180,8 @@ async function refresh() {
for (const b of items) {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${b.BeaconID}</td>
<td>${escapeHtml(b.BeaconName||"")}</td>
<td>${b.ID}</td>
<td>${escapeHtml(b.Name||"")}</td>
<td>${escapeHtml(b.UUID||"")}</td>
<td>${escapeHtml(b.NamespaceId||"")}</td>
<td>${escapeHtml(b.InstanceId||"")}</td>
@ -194,15 +194,15 @@ async function refresh() {
}
async function saveBeacon() {
const name = (document.getElementById("BeaconName").value || "").trim();
const name = (document.getElementById("Name").value || "").trim();
if (!name) {
show({ OK:false, ERROR:"missing_beacon_name", MESSAGE:"BeaconName is required" });
show({ OK:false, ERROR:"missing_beacon_name", MESSAGE:"Name is required" });
return;
}
const body = {
BeaconID: valIntOrNull("BeaconID"),
BeaconName: name,
Name: name,
UUID: (document.getElementById("UUID").value || "").trim(),
NamespaceId: (document.getElementById("NamespaceId").value || "").trim(),
InstanceId: (document.getElementById("InstanceId").value || "").trim(),

View file

@ -7,16 +7,16 @@
<cfparam name="users_to_email" default="">
<cfquery name="select_users_to_email" datasource="#application.datasource#">
SELECT U.UserEmailAddress
SELECT U.EmailAddress
FROM Users U
WHERE UserID in (0,1,2)
</cfquery>
<cfoutput query="select_users_to_email">
#UserEmailAddress#,
#EmailAddress#,
<cfset users_to_email=listappend(users_to_email, #UserEmailAddress#)>
<cfset users_to_email=listappend(users_to_email, #EmailAddress#)>
</cfoutput><br><br>
@ -95,18 +95,18 @@
<cfloop index="the_email_address" list="#users_to_email#">
<cfquery name="get_user_email" datasource="#application.datasource#">
SELECT UserUUID
SELECT UUID
FROM Users
WHERE UserEmailAddress = '#the_email_address#'
WHERE EmailAddress = '#the_email_address#'
AND
UserIsEmailverified = 1
AND
UserIsContactVerified > 0
IsContactVerified > 0
</cfquery>
<cfset form.this_email_body = form.email_body & "
instant unsubscribe link: https://www.payfrit.com/remove_me.cfm?UserUUID="&#get_user_email.UserUUID#>
instant unsubscribe link: https://www.payfrit.com/remove_me.cfm?UUID="&#get_user_email.UUID#>
<cfmail to="#the_email_address#" from="admin@payfrit.com" subject="#form.email_subject#" type="HTML">
#HTMLCodeFormat(form.this_email_body)#</cfmail>

View file

@ -5,14 +5,14 @@
<cfif form.mode eq "start">
<CFQUERY name="get_verified_users" datasource="#application.datasource#">
SELECT U.UserID, U.UserEmailAddress, U.UserContactNumber,U.UserAddedOn
SELECT U.ID, U.EmailAddress, U.ContactNumber,U.AddedOn
FROM Users U
WHERE U.UserIsEmailVerified = 1
WHERE U.IsEmailVerified = 1
AND
U.UserIsCOntactVerified > 0
AND
U.UserID > 435
ORDER BY U.UserID DESC
U.ID > 435
ORDER BY U.ID DESC
</cfquery>
@ -48,16 +48,16 @@
</form>
</td>
<td>#UserEmailAddress#</td>
<td>#UserContactNumber#</td>
<td>#dateformat(UserAddedOn, "mmmm dd, YYYY")# at #timeformat(UserAddedOn, "hh:nn tt")#</td>
<td>#EmailAddress#</td>
<td>#ContactNumber#</td>
<td>#dateformat(AddedOn, "mmmm dd, YYYY")# at #timeformat(AddedOn, "hh:nn tt")#</td>
<td>
<CFQUERY name="get_orders" datasource="#application.datasource#">
SELECT O.OrderUUID
SELECT O.UUID
FROM Orders O
WHERE O.OrderUserID = #get_verified_users.UserID#
ORDER BY O.OrderID DESC
WHERE O.UserID = #get_verified_users.ID#
ORDER BY O.ID DESC
</cfquery>
<cfparam name="looper" default="">
@ -66,7 +66,7 @@
<cfloop query="get_orders">
<cfset looper=incrementvalue(looper)>
<a href="https://payfr.it/show_order.cfm?OrderUUID=#get_orders.OrderUUID#&is_admin_view=1" target="_blank">#looper#</a>, &nbsp;
<a href="https://payfr.it/show_order.cfm?UUID=#get_orders.UUID#&is_admin_view=1" target="_blank">#looper#</a>, &nbsp;
</cfloop>
</td>
@ -79,10 +79,10 @@
<cfelseif form.mode eq "user_traffic">
<CFQUERY name="get_user_traffic" datasource="#application.datasource#">
SELECT V.VisitorTrackingPageMode, V.VisitorTrackingAddedOn
FROM VisitorTracking V
WHERE V.VisitorTrackingUserID = #form.chip#
ORDER BY V.VisitorTrackingAddedOn DESC
SELECT V.PageMode, V.AddedOn
FROM VisitorTrackings V
WHERE V.UserID = #form.chip#
ORDER BY V.AddedOn DESC
</cfquery>
<table>
@ -92,8 +92,8 @@
</tr>
<cfoutput query="get_user_traffic">
<tr>
<td>#VisitorTrackingPageMode#</td>
<td>#VisitorTrackingAddedOn#</td>
<td>#PageMode#</td>
<td>#AddedOn#</td>
</tr>
</cfoutput>
</table>

View file

@ -25,7 +25,7 @@
</head>
<body>
<h2>ServicePoints</h2>
<div class="warn">Required: ServicePointName</div>
<div class="warn">Required: Name</div>
<div class="ok" id="jsStatus">(JS not loaded yet)</div>
<div class="row">
@ -38,18 +38,18 @@
</div>
<div>
<label>ServicePointName (required)</label><br>
<input id="ServicePointName" placeholder="Front Counter" required>
<label>Name (required)</label><br>
<input id="Name" placeholder="Front Counter" required>
</div>
<div>
<label>ServicePointTypeID</label><br>
<input id="ServicePointTypeID" placeholder="0">
<label>TypeID</label><br>
<input id="TypeID" placeholder="0">
</div>
<div>
<label>ServicePointCode</label><br>
<input id="ServicePointCode" placeholder="COUNTER">
<label>Code</label><br>
<input id="Code" placeholder="COUNTER">
</div>
<div>
@ -160,14 +160,14 @@ function escapeHtml(s){
}
function loadIntoForm(sp){
document.getElementById("ServicePointID").value = sp.ServicePointID || "";
document.getElementById("ServicePointName").value = sp.ServicePointName || "";
document.getElementById("ServicePointTypeID").value = (sp.ServicePointTypeID ?? 0);
document.getElementById("ServicePointCode").value = sp.ServicePointCode || "";
document.getElementById("ServicePointID").value = sp.ID || "";
document.getElementById("Name").value = sp.Name || "";
document.getElementById("TypeID").value = (sp.TypeID ?? 0);
document.getElementById("Code").value = sp.Code || "";
document.getElementById("Description").value = sp.Description || "";
document.getElementById("SortOrder").value = (sp.SortOrder ?? 0);
document.getElementById("IsActive").value = ("" + (sp.IsActive ?? 1));
document.getElementById("DelServicePointID").value = sp.ServicePointID || "";
document.getElementById("DelServicePointID").value = sp.ID || "";
}
async function refresh() {
@ -181,10 +181,10 @@ async function refresh() {
for (const sp of items) {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${sp.ServicePointID}</td>
<td>${escapeHtml(sp.ServicePointName||"")}</td>
<td>${sp.ServicePointTypeID}</td>
<td>${escapeHtml(sp.ServicePointCode||"")}</td>
<td>${sp.ID}</td>
<td>${escapeHtml(sp.Name||"")}</td>
<td>${sp.TypeID}</td>
<td>${escapeHtml(sp.Code||"")}</td>
<td>${sp.SortOrder}</td>
<td>${sp.IsActive}</td>
`;
@ -195,17 +195,17 @@ async function refresh() {
}
async function saveSP() {
const name = (document.getElementById("ServicePointName").value || "").trim();
const name = (document.getElementById("Name").value || "").trim();
if (!name) {
show({ OK:false, ERROR:"missing_servicepoint_name", MESSAGE:"ServicePointName is required" });
show({ OK:false, ERROR:"missing_servicepoint_name", MESSAGE:"Name is required" });
return;
}
const body = {
ServicePointID: valIntOrNull("ServicePointID"),
ServicePointName: name,
ServicePointTypeID: valIntOrZero("ServicePointTypeID"),
ServicePointCode: (document.getElementById("ServicePointCode").value || "").trim(),
Name: name,
TypeID: valIntOrZero("TypeID"),
Code: (document.getElementById("Code").value || "").trim(),
Description: (document.getElementById("Description").value || "").trim(),
SortOrder: valIntOrZero("SortOrder"),
IsActive: parseInt(document.getElementById("IsActive").value, 10)

View file

@ -173,6 +173,11 @@ if (len(request._api_path)) {
// Worker app endpoints
if (findNoCase("/api/workers/myBusinesses.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/workers/tierStatus.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/workers/createAccount.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/workers/onboardingLink.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/workers/earlyUnlock.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/workers/ledger.cfm", request._api_path)) request._api_isPublic = true;
// Portal endpoints
if (findNoCase("/api/portal/stats.cfm", request._api_path)) request._api_isPublic = true;
@ -193,6 +198,7 @@ if (len(request._api_path)) {
if (findNoCase("/api/menu/getForBuilder.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/menu/saveFromBuilder.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/menu/updateStations.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/menu/menus.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/menu/uploadHeader.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/menu/listCategories.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/menu/saveCategory.cfm", request._api_path)) request._api_isPublic = true;
@ -259,6 +265,9 @@ if (len(request._api_path)) {
if (findNoCase("/api/ratings/setup.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/ratings/submit.cfm", request._api_path)) request._api_isPublic = true;
// App info endpoints (public, no auth needed)
if (findNoCase("/api/app/about.cfm", request._api_path)) request._api_isPublic = true;
// Stripe endpoints
if (findNoCase("/api/stripe/onboard.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/stripe/status.cfm", request._api_path)) request._api_isPublic = true;

View file

@ -38,9 +38,11 @@ try {
// Optional fields
line2 = trim(data.Line2 ?: "");
label = trim(data.Label ?: "");
setAsDefault = (data.SetAsDefault ?: false) == true;
// Hardcoded to delivery address type
typeId = 2;
// Validation
if (len(line1) == 0 || len(city) == 0 || stateId <= 0 || len(zipCode) == 0) {
writeOutput(serializeJSON({
@ -51,16 +53,17 @@ try {
abort;
}
// If setting as default, clear other defaults first
// If setting as default, clear other defaults first (for same type)
if (setAsDefault) {
queryExecute("
UPDATE Addresses
SET AddressIsDefaultDelivery = 0
WHERE AddressUserID = :userId
AND (AddressBusinessID = 0 OR AddressBusinessID IS NULL)
AND AddressTypeID LIKE '%2%'
WHERE UserID = :userId
AND (BusinessID = 0 OR BusinessID IS NULL)
AND AddressTypeID = :typeId
", {
userId: { value: userId, cfsqltype: "cf_sql_integer" }
userId: { value: userId, cfsqltype: "cf_sql_integer" },
typeId: { value: typeId, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
}
@ -72,23 +75,23 @@ try {
queryExecute("
INSERT INTO Addresses (
AddressID,
AddressUserID,
AddressBusinessID,
UserID,
BusinessID,
AddressTypeID,
AddressLabel,
AddressIsDefaultDelivery,
AddressLine1,
AddressLine2,
AddressCity,
AddressStateID,
AddressZIPCode,
AddressIsDeleted,
AddressAddedOn
Line1,
Line2,
City,
StateID,
ZIPCode,
IsDeleted,
AddedOn
) VALUES (
:addressId,
:userId,
0,
'2',
:typeId,
:label,
:isDefault,
:line1,
@ -102,6 +105,7 @@ try {
", {
addressId: { value: newAddressId, cfsqltype: "cf_sql_integer" },
userId: { value: userId, cfsqltype: "cf_sql_integer" },
typeId: { value: typeId, cfsqltype: "cf_sql_integer" },
label: { value: label, cfsqltype: "cf_sql_varchar" },
isDefault: { value: setAsDefault ? 1 : 0, cfsqltype: "cf_sql_integer" },
line1: { value: line1, cfsqltype: "cf_sql_varchar" },
@ -113,7 +117,7 @@ try {
}, { datasource: "payfrit" });
// Get state info for response
qState = queryExecute("SELECT tt_StateAbbreviation as StateAbbreviation, tt_StateName as StateName FROM tt_States WHERE tt_StateID = :stateId", {
qState = queryExecute("SELECT Abbreviation as StateAbbreviation, Name as StateName FROM tt_States WHERE ID = :stateId", {
stateId: { value: stateId, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
@ -124,6 +128,7 @@ try {
"OK": true,
"ADDRESS": {
"AddressID": newAddressId,
"TypeID": typeId,
"Label": len(label) ? label : "Address",
"IsDefault": setAsDefault,
"Line1": line1,

View file

@ -76,11 +76,11 @@ if (addressId <= 0) {
try {
// First, get the address details so we can find all matching duplicates
qAddr = queryExecute("
SELECT AddressLine1, AddressLine2, AddressCity, AddressStateID, AddressZIPCode
SELECT Line1, Line2, City, StateID, ZIPCode
FROM Addresses
WHERE AddressID = :addressId
AND AddressUserID = :userId
AND AddressIsDeleted = 0
WHERE ID = :addressId
AND UserID = :userId
AND IsDeleted = 0
", {
addressId: { value = addressId, cfsqltype = "cf_sql_integer" },
userId: { value = userId, cfsqltype = "cf_sql_integer" }
@ -93,21 +93,21 @@ try {
// Soft-delete ALL addresses that match the same Line1, Line2, City, StateID, ZIPCode
qDelete = queryExecute("
UPDATE Addresses
SET AddressIsDeleted = 1
WHERE AddressUserID = :userId
AND AddressLine1 = :line1
AND AddressLine2 = :line2
AND AddressCity = :city
AND AddressStateID = :stateId
AND AddressZIPCode = :zip
AND AddressIsDeleted = 0
SET IsDeleted = 1
WHERE UserID = :userId
AND Line1 = :line1
AND Line2 = :line2
AND City = :city
AND StateID = :stateId
AND ZIPCode = :zip
AND IsDeleted = 0
", {
userId: { value = userId, cfsqltype = "cf_sql_integer" },
line1: { value = qAddr.AddressLine1, cfsqltype = "cf_sql_varchar", null = !len(qAddr.AddressLine1) },
line2: { value = qAddr.AddressLine2, cfsqltype = "cf_sql_varchar", null = !len(qAddr.AddressLine2) },
city: { value = qAddr.AddressCity, cfsqltype = "cf_sql_varchar", null = !len(qAddr.AddressCity) },
stateId: { value = qAddr.AddressStateID, cfsqltype = "cf_sql_integer" },
zip: { value = qAddr.AddressZIPCode, cfsqltype = "cf_sql_varchar", null = !len(qAddr.AddressZIPCode) }
line1: { value = qAddr.Line1, cfsqltype = "cf_sql_varchar", null = !len(qAddr.Line1) },
line2: { value = qAddr.Line2, cfsqltype = "cf_sql_varchar", null = !len(qAddr.Line2) },
city: { value = qAddr.City, cfsqltype = "cf_sql_varchar", null = !len(qAddr.City) },
stateId: { value = qAddr.StateID, cfsqltype = "cf_sql_integer" },
zip: { value = qAddr.ZIPCode, cfsqltype = "cf_sql_varchar", null = !len(qAddr.ZIPCode) }
});
writeOutput(serializeJSON({

View file

@ -46,26 +46,25 @@ if (userId <= 0) {
}
try {
// Get user's delivery addresses with GROUP BY to show unique addresses only
// Get user's delivery addresses
qAddresses = queryExecute("
SELECT
MIN(a.AddressID) as AddressID,
MAX(a.AddressLabel) as AddressLabel,
MAX(a.AddressIsDefaultDelivery) as AddressIsDefaultDelivery,
a.AddressLine1,
a.AddressLine2,
a.AddressCity,
a.AddressStateID,
MAX(s.tt_StateAbbreviation) as StateAbbreviation,
MAX(s.tt_StateName) as StateName,
a.AddressZIPCode
a.ID,
a.IsDefaultDelivery,
a.Line1,
a.Line2,
a.City,
a.StateID,
s.Abbreviation as StateAbbreviation,
s.Name as StateName,
a.ZIPCode
FROM Addresses a
LEFT JOIN tt_States s ON a.AddressStateID = s.tt_StateID
WHERE a.AddressUserID = :userId
AND a.AddressTypeID LIKE '%2%'
AND a.AddressIsDeleted = 0
GROUP BY a.AddressLine1, a.AddressLine2, a.AddressCity, a.AddressStateID, a.AddressZIPCode
ORDER BY MAX(a.AddressIsDefaultDelivery) DESC, MIN(a.AddressID) DESC
LEFT JOIN tt_States s ON a.StateID = s.ID
WHERE a.UserID = :userId
AND (a.BusinessID = 0 OR a.BusinessID IS NULL)
AND a.AddressTypeID = 2
AND a.IsDeleted = 0
ORDER BY a.IsDefaultDelivery DESC, a.ID DESC
", {
userId: { value = userId, cfsqltype = "cf_sql_integer" }
});
@ -73,17 +72,15 @@ try {
addresses = [];
for (row in qAddresses) {
arrayAppend(addresses, {
"AddressID": row.AddressID,
"Label": len(row.AddressLabel) ? row.AddressLabel : "Address",
"IsDefault": row.AddressIsDefaultDelivery == 1,
"Line1": row.AddressLine1,
"Line2": row.AddressLine2 ?: "",
"City": row.AddressCity,
"StateID": row.AddressStateID,
"AddressID": row.ID,
"IsDefault": row.IsDefaultDelivery == 1,
"Line1": row.Line1,
"Line2": row.Line2 ?: "",
"City": row.City,
"StateID": row.StateID,
"StateAbbr": row.StateAbbreviation ?: "",
"StateName": row.StateName ?: "",
"ZIPCode": row.AddressZIPCode,
"DisplayText": row.AddressLine1 & (len(row.AddressLine2) ? ", " & row.AddressLine2 : "") & ", " & row.AddressCity & ", " & (row.StateAbbreviation ?: "") & " " & row.AddressZIPCode
"ZIPCode": row.ZIPCode,
"DisplayText": row.Line1 & (len(row.Line2) ? ", " & row.Line2 : "") & ", " & row.City & ", " & (row.StateAbbreviation ?: "") & " " & row.ZIPCode
});
}
@ -96,8 +93,7 @@ try {
apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": e.message,
"LINE": e.tagContext[1].line ?: 0
"MESSAGE": e.message
});
}
</cfscript>

View file

@ -41,11 +41,11 @@ try {
// Verify address belongs to user
qCheck = queryExecute("
SELECT AddressID
SELECT ID
FROM Addresses
WHERE AddressID = :addressId
AND AddressUserID = :userId
AND AddressIsDeleted = 0
WHERE ID = :addressId
AND UserID = :userId
AND IsDeleted = 0
", {
addressId: { value: addressId, cfsqltype: "cf_sql_integer" },
userId: { value: userId, cfsqltype: "cf_sql_integer" }
@ -64,8 +64,8 @@ try {
queryExecute("
UPDATE Addresses
SET AddressIsDefaultDelivery = 0
WHERE AddressUserID = :userId
AND (AddressBusinessID = 0 OR AddressBusinessID IS NULL)
WHERE UserID = :userId
AND (BusinessID = 0 OR BusinessID IS NULL)
AND AddressTypeID LIKE '%2%'
", {
userId: { value: userId, cfsqltype: "cf_sql_integer" }
@ -75,7 +75,7 @@ try {
queryExecute("
UPDATE Addresses
SET AddressIsDefaultDelivery = 1
WHERE AddressID = :addressId
WHERE ID = :addressId
", {
addressId: { value: addressId, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });

View file

@ -6,9 +6,9 @@
<cfscript>
try {
qStates = queryExecute("
SELECT tt_StateID as StateID, tt_StateAbbreviation as StateAbbreviation, tt_StateName as StateName
SELECT tt_StateID as StateID, Abbreviation as StateAbbreviation, Name as StateName
FROM tt_States
ORDER BY tt_StateName
ORDER BY Name
", {}, { datasource: "payfrit" });
states = [];

40
api/addresses/types.cfm Normal file
View file

@ -0,0 +1,40 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfheader name="Cache-Control" value="max-age=3600">
<cfscript>
/**
* Get list of address types
* GET: /api/addresses/types.cfm
* Returns: { OK: true, TYPES: [{ ID: 1, Label: "Billing" }, ...] }
*/
try {
qTypes = queryExecute("
SELECT tt_AddressTypeID as ID, tt_AddressType as Label
FROM tt_AddressTypes
ORDER BY tt_AddressTypeID
", {}, { datasource: "payfrit" });
types = [];
for (row in qTypes) {
arrayAppend(types, {
"ID": row.ID,
"Label": row.Label
});
}
writeOutput(serializeJSON({
"OK": true,
"TYPES": types
}));
} catch (any e) {
writeOutput(serializeJSON({
"OK": false,
"ERROR": "server_error",
"MESSAGE": e.message
}));
}
</cfscript>

View file

@ -0,0 +1,37 @@
<cfsetting showdebugoutput="false">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
// Add IsActive column to TaskCategories table
try {
// Check if column exists
qCheck = queryExecute("
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'payfrit'
AND TABLE_NAME = 'TaskCategories'
AND COLUMN_NAME = 'IsActive'
", [], { datasource: "payfrit" });
if (qCheck.recordCount == 0) {
queryExecute("
ALTER TABLE TaskCategories
ADD COLUMN IsActive TINYINT(1) NOT NULL DEFAULT 1
", [], { datasource: "payfrit" });
writeOutput(serializeJSON({
"OK": true,
"MESSAGE": "Column IsActive added to TaskCategories"
}));
} else {
writeOutput(serializeJSON({
"OK": true,
"MESSAGE": "Column already exists"
}));
}
} catch (any e) {
writeOutput(serializeJSON({
"OK": false,
"ERROR": e.message
}));
}
</cfscript>

View file

@ -8,9 +8,9 @@
* Add Schedule Fields to Categories Table
*
* Adds time-based scheduling fields:
* - CategoryScheduleStart: TIME - Start time when category is available (e.g., 06:00:00 for breakfast)
* - CategoryScheduleEnd: TIME - End time when category stops being available (e.g., 11:00:00)
* - CategoryScheduleDays: VARCHAR(20) - Comma-separated list of day IDs (1=Sun, 2=Mon, etc.) or NULL for all days
* - ScheduleStart: TIME - Start time when category is available (e.g., 06:00:00 for breakfast)
* - ScheduleEnd: TIME - End time when category stops being available (e.g., 11:00:00)
* - ScheduleDays: VARCHAR(20) - Comma-separated list of day IDs (1=Sun, 2=Mon, etc.) or NULL for all days
*
* Run this once to migrate the schema.
*/
@ -24,38 +24,38 @@ try {
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'payfrit'
AND TABLE_NAME = 'Categories'
AND COLUMN_NAME IN ('CategoryScheduleStart', 'CategoryScheduleEnd', 'CategoryScheduleDays')
AND COLUMN_NAME IN ('ScheduleStart', 'ScheduleEnd', 'ScheduleDays')
", {}, { datasource: "payfrit" });
existingCols = valueList(qCheck.COLUMN_NAME);
added = [];
// Add CategoryScheduleStart if not exists
if (!listFindNoCase(existingCols, "CategoryScheduleStart")) {
// Add ScheduleStart if not exists
if (!listFindNoCase(existingCols, "ScheduleStart")) {
queryExecute("
ALTER TABLE Categories
ADD COLUMN CategoryScheduleStart TIME NULL
ADD COLUMN ScheduleStart TIME NULL
", {}, { datasource: "payfrit" });
arrayAppend(added, "CategoryScheduleStart");
arrayAppend(added, "ScheduleStart");
}
// Add CategoryScheduleEnd if not exists
if (!listFindNoCase(existingCols, "CategoryScheduleEnd")) {
// Add ScheduleEnd if not exists
if (!listFindNoCase(existingCols, "ScheduleEnd")) {
queryExecute("
ALTER TABLE Categories
ADD COLUMN CategoryScheduleEnd TIME NULL
ADD COLUMN ScheduleEnd TIME NULL
", {}, { datasource: "payfrit" });
arrayAppend(added, "CategoryScheduleEnd");
arrayAppend(added, "ScheduleEnd");
}
// Add CategoryScheduleDays if not exists
if (!listFindNoCase(existingCols, "CategoryScheduleDays")) {
// Add ScheduleDays if not exists
if (!listFindNoCase(existingCols, "ScheduleDays")) {
queryExecute("
ALTER TABLE Categories
ADD COLUMN CategoryScheduleDays VARCHAR(20) NULL
ADD COLUMN ScheduleDays VARCHAR(20) NULL
", {}, { datasource: "payfrit" });
arrayAppend(added, "CategoryScheduleDays");
arrayAppend(added, "ScheduleDays");
}
response["OK"] = true;

View file

@ -15,8 +15,8 @@ try {
// Find the Fountain Soda item we created
qFountain = queryExecute("
SELECT ItemID, ItemName FROM Items
WHERE ItemBusinessID = :bizId AND ItemName = 'Fountain Soda'
SELECT ID, Name FROM Items
WHERE BusinessID = :bizId AND Name = 'Fountain Soda'
", { bizId: bigDeansBusinessId }, { datasource: "payfrit" });
if (qFountain.recordCount == 0) {
@ -31,13 +31,13 @@ try {
// Update Fountain Soda to require child selection and be collapsible
queryExecute("
UPDATE Items
SET ItemRequiresChildSelection = 1, ItemIsCollapsible = 1
SET RequiresChildSelection = 1, IsCollapsible = 1
WHERE ItemID = :itemId
", { itemId: fountainId }, { datasource: "payfrit" });
// Check if modifiers already exist
qExisting = queryExecute("
SELECT COUNT(*) as cnt FROM Items WHERE ItemParentItemID = :parentId
SELECT COUNT(*) as cnt FROM Items WHERE ParentItemID = :parentId
", { parentId: fountainId }, { datasource: "payfrit" });
if (qExisting.cnt > 0) {
@ -53,10 +53,10 @@ try {
queryExecute("
INSERT INTO Items (
ItemID, ItemBusinessID, ItemCategoryID, ItemParentItemID,
ItemName, ItemDescription, ItemPrice, ItemIsActive,
ItemSortOrder, ItemIsCollapsible, ItemRequiresChildSelection,
ItemMaxNumSelectionReq, ItemAddedOn
ItemID, BusinessID, CategoryID, ParentItemID,
Name, Description, Price, IsActive,
SortOrder, IsCollapsible, RequiresChildSelection,
MaxNumSelectionReq, AddedOn
) VALUES (
:itemId, :bizId, 0, :parentId,
'Size', 'Choose your size', 0, 1,
@ -80,10 +80,10 @@ try {
qMaxItem = queryExecute("SELECT COALESCE(MAX(ItemID), 0) + 1 as nextId FROM Items", {}, { datasource: "payfrit" });
queryExecute("
INSERT INTO Items (
ItemID, ItemBusinessID, ItemCategoryID, ItemParentItemID,
ItemName, ItemDescription, ItemPrice, ItemIsActive,
ItemSortOrder, ItemIsCollapsible, ItemIsCheckedByDefault,
ItemAddedOn
ItemID, BusinessID, CategoryID, ParentItemID,
Name, Description, Price, IsActive,
SortOrder, IsCollapsible, IsCheckedByDefault,
AddedOn
) VALUES (
:itemId, :bizId, 0, :parentId,
:name, '', :price, 1,
@ -108,10 +108,10 @@ try {
queryExecute("
INSERT INTO Items (
ItemID, ItemBusinessID, ItemCategoryID, ItemParentItemID,
ItemName, ItemDescription, ItemPrice, ItemIsActive,
ItemSortOrder, ItemIsCollapsible, ItemRequiresChildSelection,
ItemMaxNumSelectionReq, ItemAddedOn
ItemID, BusinessID, CategoryID, ParentItemID,
Name, Description, Price, IsActive,
SortOrder, IsCollapsible, RequiresChildSelection,
MaxNumSelectionReq, AddedOn
) VALUES (
:itemId, :bizId, 0, :parentId,
'Flavor', 'Choose your drink', 0, 1,
@ -139,10 +139,10 @@ try {
qMaxItem = queryExecute("SELECT COALESCE(MAX(ItemID), 0) + 1 as nextId FROM Items", {}, { datasource: "payfrit" });
queryExecute("
INSERT INTO Items (
ItemID, ItemBusinessID, ItemCategoryID, ItemParentItemID,
ItemName, ItemDescription, ItemPrice, ItemIsActive,
ItemSortOrder, ItemIsCollapsible, ItemIsCheckedByDefault,
ItemAddedOn
ItemID, BusinessID, CategoryID, ParentItemID,
Name, Description, Price, IsActive,
SortOrder, IsCollapsible, IsCheckedByDefault,
AddedOn
) VALUES (
:itemId, :bizId, 0, :parentId,
:name, '', 0, 1,

View file

@ -5,7 +5,7 @@
<cfscript>
/**
* Add ItemCategoryID column to Items table
* Add CategoryID column to Items table
*/
response = { "OK": false };
@ -17,30 +17,30 @@ try {
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'payfrit'
AND TABLE_NAME = 'Items'
AND COLUMN_NAME = 'ItemCategoryID'
AND COLUMN_NAME = 'CategoryID'
", {}, { datasource: "payfrit" });
if (qCheck.recordCount > 0) {
response["OK"] = true;
response["MESSAGE"] = "ItemCategoryID column already exists";
response["MESSAGE"] = "CategoryID column already exists";
} else {
// Add the column
queryExecute("
ALTER TABLE Items
ADD COLUMN ItemCategoryID INT NULL DEFAULT 0 AFTER ItemParentItemID
ADD COLUMN CategoryID INT NULL DEFAULT 0 AFTER ParentItemID
", {}, { datasource: "payfrit" });
// Add index for performance
try {
queryExecute("
CREATE INDEX idx_items_categoryid ON Items(ItemCategoryID)
CREATE INDEX idx_items_categoryid ON Items(CategoryID)
", {}, { datasource: "payfrit" });
} catch (any indexErr) {
// Index might already exist
}
response["OK"] = true;
response["MESSAGE"] = "ItemCategoryID column added successfully";
response["MESSAGE"] = "CategoryID column added successfully";
}
} catch (any e) {

View file

@ -6,7 +6,7 @@
try {
// Check if columns already exist
checkCols = queryExecute(
"SHOW COLUMNS FROM Addresses LIKE 'AddressLat'",
"SHOW COLUMNS FROM Addresses LIKE 'Latitude'",
[],
{ datasource = "payfrit" }
);
@ -15,8 +15,8 @@ try {
// Add the columns
queryExecute(
"ALTER TABLE Addresses
ADD COLUMN AddressLat DECIMAL(10,7) NULL,
ADD COLUMN AddressLng DECIMAL(10,7) NULL",
ADD COLUMN Latitude DECIMAL(10,7) NULL,
ADD COLUMN Longitude DECIMAL(10,7) NULL",
[],
{ datasource = "payfrit" }
);

View file

@ -0,0 +1,37 @@
<cfsetting showdebugoutput="false">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
// Add CategoryID column to tt_TaskTypes (Services) table
try {
// Check if column exists
qCheck = queryExecute("
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'payfrit'
AND TABLE_NAME = 'tt_TaskTypes'
AND COLUMN_NAME = 'TaskCategoryID'
", [], { datasource: "payfrit" });
if (qCheck.recordCount == 0) {
queryExecute("
ALTER TABLE tt_TaskTypes
ADD COLUMN TaskCategoryID INT NULL
", [], { datasource: "payfrit" });
writeOutput(serializeJSON({
"OK": true,
"MESSAGE": "Column TaskCategoryID added to tt_TaskTypes"
}));
} else {
writeOutput(serializeJSON({
"OK": true,
"MESSAGE": "Column already exists"
}));
}
} catch (any e) {
writeOutput(serializeJSON({
"OK": false,
"ERROR": e.message
}));
}
</cfscript>

View file

@ -3,7 +3,7 @@
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
// Add TaskSourceType and TaskSourceID columns to Tasks table
// Add SourceType and SourceID columns to Tasks table
// These are needed for chat persistence feature
result = { "OK": true, "STEPS": [] };
@ -15,30 +15,30 @@ try {
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'payfrit'
AND TABLE_NAME = 'Tasks'
AND COLUMN_NAME IN ('TaskSourceType', 'TaskSourceID')
AND COLUMN_NAME IN ('SourceType', 'SourceID')
", [], { datasource: "payfrit" });
existingCols = valueList(cols.COLUMN_NAME);
arrayAppend(result.STEPS, "Existing columns: #existingCols#");
// Add TaskSourceType if missing
if (!listFindNoCase(existingCols, "TaskSourceType")) {
// Add SourceType if missing
if (!listFindNoCase(existingCols, "SourceType")) {
queryExecute("
ALTER TABLE Tasks ADD COLUMN TaskSourceType VARCHAR(50) NULL
ALTER TABLE Tasks ADD COLUMN SourceType VARCHAR(50) NULL
", [], { datasource: "payfrit" });
arrayAppend(result.STEPS, "Added TaskSourceType column");
arrayAppend(result.STEPS, "Added SourceType column");
} else {
arrayAppend(result.STEPS, "TaskSourceType already exists");
arrayAppend(result.STEPS, "SourceType already exists");
}
// Add TaskSourceID if missing
if (!listFindNoCase(existingCols, "TaskSourceID")) {
// Add SourceID if missing
if (!listFindNoCase(existingCols, "SourceID")) {
queryExecute("
ALTER TABLE Tasks ADD COLUMN TaskSourceID INT NULL
ALTER TABLE Tasks ADD COLUMN SourceID INT NULL
", [], { datasource: "payfrit" });
arrayAppend(result.STEPS, "Added TaskSourceID column");
arrayAppend(result.STEPS, "Added SourceID column");
} else {
arrayAppend(result.STEPS, "TaskSourceID already exists");
arrayAppend(result.STEPS, "SourceID already exists");
}
// Verify columns now exist
@ -47,7 +47,7 @@ try {
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'payfrit'
AND TABLE_NAME = 'Tasks'
AND COLUMN_NAME IN ('TaskSourceType', 'TaskSourceID')
AND COLUMN_NAME IN ('SourceType', 'SourceID')
", [], { datasource: "payfrit" });
result.COLUMNS = [];

View file

@ -6,49 +6,49 @@
// Show all beacons with their current business/service point assignments
q = queryExecute("
SELECT
b.BeaconID,
b.BeaconUUID,
b.BeaconName,
lt.BusinessID,
lt.ServicePointID,
biz.BusinessName,
sp.ServicePointName
b.ID,
b.UUID,
b.Name,
sp_link.BusinessID,
sp_link.ID,
biz.Name,
sp.Name
FROM Beacons b
LEFT JOIN lt_Beacon_Businesses_ServicePoints lt ON lt.BeaconID = b.BeaconID
LEFT JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
WHERE b.BeaconIsActive = 1
ORDER BY b.BeaconID
LEFT JOIN ServicePoints sp_link ON sp_link.BeaconID = b.ID
LEFT JOIN Businesses biz ON biz.ID = sp_link.BusinessID
LEFT JOIN ServicePoints sp ON sp.ID = sp_link.ID
WHERE b.IsActive = 1
ORDER BY b.ID
", {}, { datasource: "payfrit" });
rows = [];
for (row in q) {
arrayAppend(rows, {
"BeaconID": row.BeaconID,
"BeaconUUID": row.BeaconUUID,
"BeaconName": row.BeaconName ?: "",
"BeaconID": row.ID,
"UUID": row.UUID,
"Name": row.Name ?: "",
"BusinessID": row.BusinessID ?: 0,
"BusinessName": row.BusinessName ?: "",
"Name": row.Name ?: "",
"ServicePointID": row.ServicePointID ?: 0,
"ServicePointName": row.ServicePointName ?: ""
"Name": row.Name ?: ""
});
}
// Also get service points for reference
spQuery = queryExecute("
SELECT sp.ServicePointID, sp.ServicePointName, sp.ServicePointBusinessID, b.BusinessName
SELECT sp.ID, sp.Name, sp.BusinessID, b.Name
FROM ServicePoints sp
JOIN Businesses b ON b.BusinessID = sp.ServicePointBusinessID
ORDER BY sp.ServicePointBusinessID, sp.ServicePointID
JOIN Businesses b ON b.ID = sp.BusinessID
ORDER BY sp.BusinessID, sp.ID
", {}, { datasource: "payfrit" });
servicePoints = [];
for (sp in spQuery) {
arrayAppend(servicePoints, {
"ServicePointID": sp.ServicePointID,
"ServicePointName": sp.ServicePointName,
"BusinessID": sp.ServicePointBusinessID,
"BusinessName": sp.BusinessName
"ServicePointID": sp.ID,
"Name": sp.Name,
"BusinessID": sp.BusinessID,
"Name": sp.Name
});
}

View file

@ -5,16 +5,16 @@
<cfscript>
// Check Big Dean's owner
q = queryExecute("
SELECT b.BusinessID, b.BusinessName, b.BusinessUserID
SELECT b.ID, b.Name, b.UserID
FROM Businesses b
WHERE b.BusinessID = 27
WHERE b.ID = 27
", {}, { datasource: "payfrit" });
// Get users
users = queryExecute("
SELECT *
FROM Users
ORDER BY UserID
ORDER BY ID
LIMIT 20
", {}, { datasource: "payfrit" });
@ -23,9 +23,9 @@ colNames = users.getColumnNames();
writeOutput(serializeJSON({
"OK": true,
"BigDeans": {
"BusinessID": q.BusinessID,
"BusinessName": q.BusinessName,
"BusinessUserID": q.BusinessUserID
"BusinessID": q.ID,
"Name": q.Name,
"UserID": q.UserID
},
"UserColumns": colNames,
"UserCount": users.recordCount

View file

@ -12,9 +12,9 @@ if (!len(phone)) {
}
q = queryExecute("
SELECT UserID, UserFirstName, UserLastName, UserEmail, UserPhone, UserIsContactVerified
SELECT ID, FirstName, LastName, EmailAddress, ContactNumber, IsContactVerified
FROM Users
WHERE UserPhone = :phone OR UserEmail = :phone
WHERE ContactNumber = :phone OR EmailAddress = :phone
LIMIT 1
", { phone: phone }, { datasource: "payfrit" });
@ -25,11 +25,11 @@ if (q.recordCount EQ 0) {
writeOutput(serializeJSON({
"OK": true,
"UserID": q.UserID,
"FirstName": q.UserFirstName,
"LastName": q.UserLastName,
"Email": q.UserEmail,
"Phone": q.UserPhone,
"Verified": q.UserIsContactVerified
"UserID": q.ID,
"FirstName": q.FirstName,
"LastName": q.LastName,
"Email": q.EmailAddress,
"Phone": q.ContactNumber,
"Verified": q.IsContactVerified
}));
</cfscript>

View file

@ -5,7 +5,8 @@
<cfscript>
/**
* Cleanup Lazy Daisy Beacons
* - Removes duplicate beacons created by setupBeaconTables
* - Unassigns beacons 7, 8, 9 from service points
* - Deletes beacons 7, 8, 9
* - Updates original beacons with proper names
*/
response = { "OK": false, "steps": [] };
@ -13,52 +14,50 @@ response = { "OK": false, "steps": [] };
try {
lazyDaisyID = 37;
// Delete duplicate assignments for beacons 7, 8, 9
// Unassign beacons 7, 8, 9 from any service points
queryExecute("
DELETE FROM lt_Beacon_Businesses_ServicePoints
UPDATE ServicePoints
SET BeaconID = NULL, AssignedByUserID = NULL
WHERE BeaconID IN (7, 8, 9) AND BusinessID = :bizId
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
response.steps.append("Deleted duplicate assignments for beacons 7, 8, 9");
response.steps.append("Unassigned beacons 7, 8, 9 from service points");
// Delete duplicate beacons 7, 8, 9
queryExecute("
DELETE FROM Beacons
WHERE BeaconID IN (7, 8, 9) AND BeaconBusinessID = :bizId
WHERE ID IN (7, 8, 9) AND BusinessID = :bizId
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
response.steps.append("Deleted duplicate beacons 7, 8, 9");
// Update original beacons with names based on their service point assignments
// Beacon 4 -> Table 1 (ServicePointID 4)
// Beacon 5 -> Table 2 (ServicePointID 5)
// Beacon 6 -> Table 3 (ServicePointID 6)
queryExecute("
UPDATE Beacons SET BeaconName = 'Beacon - Table 1'
WHERE BeaconID = 4 AND BeaconBusinessID = :bizId
UPDATE Beacons SET Name = 'Beacon - Table 1'
WHERE ID = 4 AND BusinessID = :bizId
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
response.steps.append("Updated Beacon 4 name to 'Beacon - Table 1'");
queryExecute("
UPDATE Beacons SET BeaconName = 'Beacon - Table 2'
WHERE BeaconID = 5 AND BeaconBusinessID = :bizId
UPDATE Beacons SET Name = 'Beacon - Table 2'
WHERE ID = 5 AND BusinessID = :bizId
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
response.steps.append("Updated Beacon 5 name to 'Beacon - Table 2'");
queryExecute("
UPDATE Beacons SET BeaconName = 'Beacon - Table 3'
WHERE BeaconID = 6 AND BeaconBusinessID = :bizId
UPDATE Beacons SET Name = 'Beacon - Table 3'
WHERE ID = 6 AND BusinessID = :bizId
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
response.steps.append("Updated Beacon 6 name to 'Beacon - Table 3'");
// Get final status
qFinal = queryExecute("
SELECT lt.BeaconID, b.BeaconUUID, b.BeaconName, lt.BusinessID, biz.BusinessName, lt.ServicePointID, sp.ServicePointName
FROM lt_Beacon_Businesses_ServicePoints lt
JOIN Beacons b ON b.BeaconID = lt.BeaconID
JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
WHERE lt.BusinessID = :bizId
ORDER BY lt.BeaconID
SELECT sp.ID AS ServicePointID, sp.BeaconID, sp.BusinessID,
b.Name AS BeaconName, b.UUID, sp.Name AS ServicePointName,
biz.Name AS BusinessName
FROM ServicePoints sp
JOIN Beacons b ON b.ID = sp.BeaconID
JOIN Businesses biz ON biz.ID = sp.BusinessID
WHERE sp.BusinessID = :bizId AND sp.BeaconID IS NOT NULL
ORDER BY sp.BeaconID
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
beacons = [];
@ -66,8 +65,9 @@ try {
arrayAppend(beacons, {
"BeaconID": qFinal.BeaconID[i],
"BeaconName": qFinal.BeaconName[i],
"UUID": qFinal.BeaconUUID[i],
"UUID": qFinal.UUID[i],
"BusinessName": qFinal.BusinessName[i],
"ServicePointID": qFinal.ServicePointID[i],
"ServicePointName": qFinal.ServicePointName[i]
});
}

View file

@ -13,10 +13,10 @@
* Cleanup Categories - Final step after migration verification
*
* This script:
* 1. Verifies all Items have ItemBusinessID set
* 1. Verifies all Items have BusinessID set
* 2. Finds orphan items (ParentID=0, no children, not in links)
* 3. Drops ItemCategoryID column
* 4. Drops ItemIsModifierTemplate column (derived from ItemTemplateLinks now)
* 3. Drops CategoryID column
* 4. Drops IsModifierTemplate column (derived from lt_ItemID_TemplateItemID now)
* 5. Drops Categories table
*
* Query param: ?confirm=YES to actually execute (otherwise shows verification only)
@ -30,7 +30,7 @@ try {
// Verification Step 1: Check for items without BusinessID
qNoBusinessID = queryExecute("
SELECT COUNT(*) as cnt FROM Items
WHERE ItemBusinessID IS NULL OR ItemBusinessID = 0
WHERE BusinessID IS NULL OR BusinessID = 0
", {}, { datasource: "payfrit" });
response.verification["itemsWithoutBusinessID"] = qNoBusinessID.cnt;
@ -46,38 +46,38 @@ try {
qCategoryItems = queryExecute("
SELECT COUNT(DISTINCT p.ItemID) as cnt
FROM Items p
INNER JOIN Items c ON c.ItemParentItemID = p.ItemID
WHERE p.ItemParentItemID = 0
AND p.ItemBusinessID > 0
INNER JOIN Items c ON c.ParentItemID = p.ItemID
WHERE p.ParentItemID = 0
AND p.BusinessID > 0
AND NOT EXISTS (
SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = p.ItemID
SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = p.ItemID
)
", {}, { datasource: "payfrit" });
response.verification["categoryItemsCreated"] = qCategoryItems.cnt;
// Verification Step 4: Check templates exist (in ItemTemplateLinks)
// Verification Step 4: Check templates exist (in lt_ItemID_TemplateItemID)
qTemplates = queryExecute("
SELECT COUNT(DISTINCT tl.TemplateItemID) as cnt
FROM ItemTemplateLinks tl
FROM lt_ItemID_TemplateItemID tl
INNER JOIN Items t ON t.ItemID = tl.TemplateItemID
", {}, { datasource: "payfrit" });
response.verification["templatesInLinks"] = qTemplates.cnt;
// Verification Step 5: Find orphans at ParentID=0
// Orphan = ParentID=0, no children pointing to it, not in ItemTemplateLinks
// Orphan = ParentID=0, no children pointing to it, not in lt_ItemID_TemplateItemID
qOrphans = queryExecute("
SELECT i.ItemID, i.ItemName, i.ItemBusinessID
SELECT i.ID, i.Name, i.BusinessID
FROM Items i
WHERE i.ItemParentItemID = 0
WHERE i.ParentItemID = 0
AND NOT EXISTS (
SELECT 1 FROM Items child WHERE child.ItemParentItemID = i.ItemID
SELECT 1 FROM Items child WHERE child.ParentItemID = i.ID
)
AND NOT EXISTS (
SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = i.ItemID
SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = i.ID
)
ORDER BY i.ItemBusinessID, i.ItemName
ORDER BY i.BusinessID, i.Name
", {}, { datasource: "payfrit" });
response.verification["orphanCount"] = qOrphans.recordCount;
@ -85,8 +85,8 @@ try {
for (orphan in qOrphans) {
arrayAppend(response.orphans, {
"ItemID": orphan.ItemID,
"ItemName": orphan.ItemName,
"BusinessID": orphan.ItemBusinessID
"Name": orphan.Name,
"BusinessID": orphan.BusinessID
});
}
@ -96,7 +96,7 @@ try {
if (!safeToCleanup) {
arrayAppend(response.steps, "VERIFICATION FAILED - Cannot cleanup yet");
arrayAppend(response.steps, "- " & qNoBusinessID.cnt & " items still missing ItemBusinessID");
arrayAppend(response.steps, "- " & qNoBusinessID.cnt & " items still missing BusinessID");
response["OK"] = false;
writeOutput(serializeJSON(response));
abort;
@ -119,31 +119,31 @@ try {
// Execute cleanup
arrayAppend(response.steps, "Executing cleanup...");
// Step 1: Drop ItemCategoryID column
// Step 1: Drop CategoryID column
try {
queryExecute("
ALTER TABLE Items DROP COLUMN ItemCategoryID
ALTER TABLE Items DROP COLUMN CategoryID
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Dropped ItemCategoryID column from Items");
arrayAppend(response.steps, "Dropped CategoryID column from Items");
} catch (any e) {
if (findNoCase("check that column", e.message) || findNoCase("Unknown column", e.message)) {
arrayAppend(response.steps, "ItemCategoryID column already dropped");
arrayAppend(response.steps, "CategoryID column already dropped");
} else {
arrayAppend(response.steps, "Warning dropping ItemCategoryID: " & e.message);
arrayAppend(response.steps, "Warning dropping CategoryID: " & e.message);
}
}
// Step 2: Drop ItemIsModifierTemplate column (now derived from ItemTemplateLinks)
// Step 2: Drop IsModifierTemplate column (now derived from lt_ItemID_TemplateItemID)
try {
queryExecute("
ALTER TABLE Items DROP COLUMN ItemIsModifierTemplate
ALTER TABLE Items DROP COLUMN IsModifierTemplate
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Dropped ItemIsModifierTemplate column from Items");
arrayAppend(response.steps, "Dropped IsModifierTemplate column from Items");
} catch (any e) {
if (findNoCase("check that column", e.message) || findNoCase("Unknown column", e.message)) {
arrayAppend(response.steps, "ItemIsModifierTemplate column already dropped");
arrayAppend(response.steps, "IsModifierTemplate column already dropped");
} else {
arrayAppend(response.steps, "Warning dropping ItemIsModifierTemplate: " & e.message);
arrayAppend(response.steps, "Warning dropping IsModifierTemplate: " & e.message);
}
}

View file

@ -34,8 +34,8 @@ if (businessId <= 0) {
try {
// Find duplicate UserIDs for this business (keep the one with highest status or oldest)
qDupes = queryExecute("
SELECT UserID, COUNT(*) as cnt, MIN(EmployeeID) as keepId
FROM lt_Users_Businesses_Employees
SELECT ID, COUNT(*) as cnt, MIN(ID) as keepId
FROM Employees
WHERE BusinessID = ?
GROUP BY UserID
HAVING COUNT(*) > 1
@ -45,8 +45,8 @@ try {
for (row in qDupes) {
// Delete all but the one we want to keep (the one with lowest EmployeeID)
qDel = queryExecute("
DELETE FROM lt_Users_Businesses_Employees
WHERE BusinessID = ? AND UserID = ? AND EmployeeID != ?
DELETE FROM Employees
WHERE BusinessID = ? AND UserID = ? AND ID != ?
", [
{ value: businessId, cfsqltype: "cf_sql_integer" },
{ value: row.UserID, cfsqltype: "cf_sql_integer" },
@ -57,19 +57,19 @@ try {
// Get remaining employees
qRemaining = queryExecute("
SELECT e.EmployeeID, e.UserID, u.UserFirstName, u.UserLastName
FROM lt_Users_Businesses_Employees e
JOIN Users u ON e.UserID = u.UserID
SELECT e.ID, e.UserID, u.FirstName, u.LastName
FROM Employees e
JOIN Users u ON e.UserID = u.ID
WHERE e.BusinessID = ?
ORDER BY e.EmployeeID
ORDER BY e.ID
", [{ value: businessId, cfsqltype: "cf_sql_integer" }], { datasource: "payfrit" });
remaining = [];
for (r in qRemaining) {
arrayAppend(remaining, {
"EmployeeID": r.EmployeeID,
"EmployeeID": r.ID,
"UserID": r.UserID,
"Name": trim(r.UserFirstName & " " & r.UserLastName)
"Name": trim(r.FirstName & " " & r.LastName)
});
}

View file

@ -9,106 +9,120 @@ try {
// Keep only Lazy Daisy (BusinessID 37)
keepBusinessID = 37;
// First, reassign all beacons to Lazy Daisy
// Unassign all beacons from service points of other businesses
queryExecute("
UPDATE lt_Beacon_Businesses_ServicePoints
SET BusinessID = :keepID
UPDATE ServicePoints
SET BeaconID = NULL, AssignedByUserID = NULL
WHERE BusinessID != :keepID AND BeaconID IS NOT NULL
", { keepID: keepBusinessID }, { datasource: "payfrit" });
response.steps.append("Reassigned all beacons to Lazy Daisy");
response.steps.append("Unassigned beacons from other businesses' service points");
// Get list of businesses to delete
qBiz = queryExecute("
SELECT BusinessID, BusinessName FROM Businesses WHERE BusinessID != :keepID
SELECT ID, Name FROM Businesses WHERE ID != :keepID
", { keepID: keepBusinessID }, { datasource: "payfrit" });
deletedBusinesses = [];
for (i = 1; i <= qBiz.recordCount; i++) {
arrayAppend(deletedBusinesses, qBiz.BusinessName[i]);
arrayAppend(deletedBusinesses, qBiz.Name[i]);
}
response.steps.append("Found " & qBiz.recordCount & " businesses to delete");
// Delete related data first (foreign key constraints)
// Delete ItemTemplateLinks for items from other businesses
// Delete lt_ItemID_TemplateItemID for items from other businesses
queryExecute("
DELETE itl FROM ItemTemplateLinks itl
JOIN Items i ON i.ItemID = itl.ItemID
WHERE i.ItemBusinessID != :keepID
DELETE itl FROM lt_ItemID_TemplateItemID itl
JOIN Items i ON i.ID = itl.ItemID
WHERE i.BusinessID != :keepID
", { keepID: keepBusinessID }, { datasource: "payfrit" });
response.steps.append("Deleted ItemTemplateLinks for other businesses");
response.steps.append("Deleted lt_ItemID_TemplateItemID for other businesses");
// Delete Items for other businesses
qItems = queryExecute("
SELECT COUNT(*) as cnt FROM Items WHERE ItemBusinessID != :keepID
SELECT COUNT(*) as cnt FROM Items WHERE BusinessID != :keepID
", { keepID: keepBusinessID }, { datasource: "payfrit" });
queryExecute("
DELETE FROM Items WHERE ItemBusinessID != :keepID
DELETE FROM Items WHERE BusinessID != :keepID
", { keepID: keepBusinessID }, { datasource: "payfrit" });
response.steps.append("Deleted " & qItems.cnt & " items from other businesses");
// Delete Categories for other businesses
queryExecute("
DELETE FROM Categories WHERE CategoryBusinessID != :keepID
DELETE FROM Categories WHERE BusinessID != :keepID
", { keepID: keepBusinessID }, { datasource: "payfrit" });
response.steps.append("Deleted categories from other businesses");
// Delete Hours for other businesses
queryExecute("
DELETE FROM Hours WHERE HoursBusinessID != :keepID
DELETE FROM Hours WHERE BusinessID != :keepID
", { keepID: keepBusinessID }, { datasource: "payfrit" });
response.steps.append("Deleted hours from other businesses");
// Delete Employees for other businesses (skip if table doesn't exist)
// Delete Employees for other businesses
try {
queryExecute("
DELETE FROM Employees WHERE EmployeeBusinessID != :keepID
DELETE FROM Employees WHERE BusinessID != :keepID
", { keepID: keepBusinessID }, { datasource: "payfrit" });
response.steps.append("Deleted employees from other businesses");
} catch (any e) {
response.steps.append("Skipped employees (table may not exist)");
}
// Delete ServicePoints for other businesses (skip if table doesn't exist)
// Delete ServicePoints for other businesses
try {
queryExecute("
DELETE FROM ServicePoints WHERE ServicePointBusinessID != :keepID
DELETE FROM ServicePoints WHERE BusinessID != :keepID
", { keepID: keepBusinessID }, { datasource: "payfrit" });
response.steps.append("Deleted service points from other businesses");
} catch (any e) {
response.steps.append("Skipped service points (table may not exist)");
}
// Delete Stations for other businesses (skip if table doesn't exist)
// Delete Stations for other businesses
try {
queryExecute("
DELETE FROM Stations WHERE StationBusinessID != :keepID
DELETE FROM Stations WHERE BusinessID != :keepID
", { keepID: keepBusinessID }, { datasource: "payfrit" });
response.steps.append("Deleted stations from other businesses");
} catch (any e) {
response.steps.append("Skipped stations (table may not exist)");
}
// Delete beacon-business mappings for other businesses
try {
queryExecute("
DELETE FROM lt_BeaconsID_BusinessesID WHERE BusinessID != :keepID
", { keepID: keepBusinessID }, { datasource: "payfrit" });
response.steps.append("Deleted beacon mappings for other businesses");
} catch (any e) {
response.steps.append("Skipped beacon mappings (table may not exist)");
}
// Finally delete the businesses themselves
queryExecute("
DELETE FROM Businesses WHERE BusinessID != :keepID
DELETE FROM Businesses WHERE ID != :keepID
", { keepID: keepBusinessID }, { datasource: "payfrit" });
response.steps.append("Deleted " & arrayLen(deletedBusinesses) & " businesses");
// Get beacon status
qBeacons = queryExecute("
SELECT lt.BeaconID, b.BeaconUUID, lt.BusinessID, biz.BusinessName, lt.ServicePointID
FROM lt_Beacon_Businesses_ServicePoints lt
JOIN Beacons b ON b.BeaconID = lt.BeaconID
JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
SELECT sp.ID AS ServicePointID, sp.BeaconID, sp.BusinessID,
b.UUID, biz.Name AS BusinessName, sp.Name AS ServicePointName
FROM ServicePoints sp
JOIN Beacons b ON b.ID = sp.BeaconID
JOIN Businesses biz ON biz.ID = sp.BusinessID
WHERE sp.BeaconID IS NOT NULL
", {}, { datasource: "payfrit" });
beacons = [];
for (i = 1; i <= qBeacons.recordCount; i++) {
arrayAppend(beacons, {
"BeaconID": qBeacons.BeaconID[i],
"UUID": qBeacons.BeaconUUID[i],
"UUID": qBeacons.UUID[i],
"BusinessID": qBeacons.BusinessID[i],
"BusinessName": qBeacons.BusinessName[i],
"ServicePointID": qBeacons.ServicePointID[i]
"ServicePointID": qBeacons.ServicePointID[i],
"ServicePointName": qBeacons.ServicePointName[i]
});
}

View file

@ -7,9 +7,9 @@ param name="url.action" default="check"; // "check" or "deactivate"
// Check the item first
qItem = queryExecute("
SELECT ItemID, ItemName, ItemParentItemID, ItemIsActive, ItemIsCollapsible
SELECT ID, Name, ParentItemID, IsActive, IsCollapsible
FROM Items
WHERE ItemID = :itemId
WHERE ID = :itemId
", { itemId: url.itemId });
if (qItem.recordCount == 0) {
@ -19,25 +19,25 @@ if (qItem.recordCount == 0) {
// Get all children (direct only for display)
qChildren = queryExecute("
SELECT ItemID, ItemName
SELECT ID, Name
FROM Items
WHERE ItemParentItemID = :itemId
WHERE ParentItemID = :itemId
", { itemId: url.itemId });
childList = [];
for (row in qChildren) {
arrayAppend(childList, { "ItemID": row.ItemID, "ItemName": row.ItemName });
arrayAppend(childList, { "ItemID": row.ID, "Name": row.Name });
}
result = {
"OK": true,
"ACTION": url.action,
"ITEM": {
"ItemID": qItem.ItemID,
"ItemName": qItem.ItemName,
"ItemParentItemID": qItem.ItemParentItemID,
"ItemIsActive": qItem.ItemIsActive,
"ItemIsCollapsible": qItem.ItemIsCollapsible
"ItemID": qItem.ID,
"Name": qItem.Name,
"ParentItemID": qItem.ParentItemID,
"IsActive": qItem.IsActive,
"IsCollapsible": qItem.IsCollapsible
},
"HAS_CHILDREN": qChildren.recordCount > 0,
"CHILD_COUNT": qChildren.recordCount,
@ -48,14 +48,14 @@ if (url.action == "deactivate") {
// Deactivate children first
queryExecute("
UPDATE Items
SET ItemIsActive = 0
WHERE ItemParentItemID = :itemId
SET IsActive = 0
WHERE ParentItemID = :itemId
", { itemId: url.itemId });
// Then deactivate the parent
queryExecute("
UPDATE Items
SET ItemIsActive = 0
SET IsActive = 0
WHERE ItemID = :itemId
", { itemId: url.itemId });

View file

@ -5,13 +5,13 @@
// Delete cart orders (status 0) to reset for testing
result = queryExecute("
DELETE FROM OrderLineItems
WHERE OrderLineItemOrderID IN (
SELECT OrderID FROM Orders WHERE OrderStatusID = 0
WHERE OrderID IN (
SELECT ID FROM Orders WHERE StatusID = 0
)
", {}, { datasource = "payfrit" });
result2 = queryExecute("
DELETE FROM Orders WHERE OrderStatusID = 0
DELETE FROM Orders WHERE StatusID = 0
", {}, { datasource = "payfrit" });
writeOutput(serializeJSON({

View file

@ -10,27 +10,27 @@ try {
businessIDs = [38, 39, 40, 41, 42];
for (bizID in businessIDs) {
// Delete ItemTemplateLinks for items belonging to this business
// Delete lt_ItemID_TemplateItemID for items belonging to this business
queryExecute("
DELETE itl FROM ItemTemplateLinks itl
INNER JOIN Items i ON i.ItemID = itl.ItemID
WHERE i.ItemBusinessID = :bizID
DELETE itl FROM lt_ItemID_TemplateItemID itl
INNER JOIN Items i ON i.ID = itl.ItemID
WHERE i.BusinessID = :bizID
", { bizID: bizID }, { datasource: "payfrit" });
// Delete Items
queryExecute("DELETE FROM Items WHERE ItemBusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
queryExecute("DELETE FROM Items WHERE BusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
// Delete Categories
queryExecute("DELETE FROM Categories WHERE CategoryBusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
queryExecute("DELETE FROM Categories WHERE BusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
// Delete Hours
queryExecute("DELETE FROM Hours WHERE HoursBusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
queryExecute("DELETE FROM Hours WHERE BusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
// Delete Addresses linked to this business
queryExecute("DELETE FROM Addresses WHERE AddressBusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
queryExecute("DELETE FROM Addresses WHERE BusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
// Delete the Business itself
queryExecute("DELETE FROM Businesses WHERE BusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
queryExecute("DELETE FROM Businesses WHERE ID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
response.steps.append("Deleted business " & bizID & " and all related data");
}

View file

@ -5,9 +5,9 @@
try {
result = queryExecute("
UPDATE Tasks
SET TaskCompletedOn = NOW()
SET CompletedOn = NOW()
WHERE TaskTypeID = 2
AND TaskCompletedOn IS NULL
AND CompletedOn IS NULL
", {}, { datasource: "payfrit" });
affected = result.recordCount ?: 0;

View file

@ -15,24 +15,24 @@ try {
// First, check if Big Dean's has a Beverages/Drinks category
qExistingCat = queryExecute("
SELECT CategoryID, CategoryName FROM Categories
WHERE CategoryBusinessID = :bizId AND (CategoryName LIKE '%Drink%' OR CategoryName LIKE '%Beverage%')
SELECT ID, Name FROM Categories
WHERE BusinessID = :bizId AND (Name LIKE '%Drink%' OR Name LIKE '%Beverage%')
", { bizId: bigDeansBusinessId }, { datasource: "payfrit" });
if (qExistingCat.recordCount > 0) {
drinksCategoryId = qExistingCat.CategoryID;
response["CategoryNote"] = "Using existing category: " & qExistingCat.CategoryName;
response["CategoryNote"] = "Using existing category: " & qExistingCat.Name;
} else {
// Create a new Beverages category for Big Dean's
qMaxCat = queryExecute("SELECT COALESCE(MAX(CategoryID), 0) + 1 as nextId FROM Categories", {}, { datasource: "payfrit" });
drinksCategoryId = qMaxCat.nextId;
qMaxSort = queryExecute("
SELECT COALESCE(MAX(CategorySortOrder), 0) + 1 as nextSort FROM Categories WHERE CategoryBusinessID = :bizId
SELECT COALESCE(MAX(SortOrder), 0) + 1 as nextSort FROM Categories WHERE BusinessID = :bizId
", { bizId: bigDeansBusinessId }, { datasource: "payfrit" });
queryExecute("
INSERT INTO Categories (CategoryID, CategoryBusinessID, CategoryParentCategoryID, CategoryName, CategorySortOrder, CategoryAddedOn)
INSERT INTO Categories (CategoryID, BusinessID, ParentCategoryID, Name, SortOrder, AddedOn)
VALUES (:catId, :bizId, 0, 'Beverages', :sortOrder, NOW())
", {
catId: drinksCategoryId,
@ -61,8 +61,8 @@ try {
for (drink in drinks) {
// Check if item already exists
qExists = queryExecute("
SELECT ItemID FROM Items
WHERE ItemBusinessID = :bizId AND ItemName = :name AND ItemCategoryID = :catId
SELECT ID FROM Items
WHERE BusinessID = :bizId AND Name = :name AND CategoryID = :catId
", { bizId: bigDeansBusinessId, name: drink.name, catId: drinksCategoryId }, { datasource: "payfrit" });
if (qExists.recordCount == 0) {
@ -72,10 +72,10 @@ try {
queryExecute("
INSERT INTO Items (
ItemID, ItemBusinessID, ItemCategoryID, ItemParentItemID,
ItemName, ItemDescription, ItemPrice, ItemIsActive,
ItemSortOrder, ItemIsCollapsible, ItemRequiresChildSelection,
ItemAddedOn
ItemID, BusinessID, CategoryID, ParentItemID,
Name, Description, Price, IsActive,
SortOrder, IsCollapsible, RequiresChildSelection,
AddedOn
) VALUES (
:itemId, :bizId, :catId, 0,
:name, :desc, :price, 1,
@ -103,10 +103,10 @@ try {
qMaxOpt = queryExecute("SELECT COALESCE(MAX(ItemID), 0) + 1 as nextId FROM Items", {}, { datasource: "payfrit" });
queryExecute("
INSERT INTO Items (
ItemID, ItemBusinessID, ItemCategoryID, ItemParentItemID,
ItemName, ItemDescription, ItemPrice, ItemIsActive,
ItemSortOrder, ItemIsCollapsible, ItemIsCheckedByDefault,
ItemAddedOn
ItemID, BusinessID, CategoryID, ParentItemID,
Name, Description, Price, IsActive,
SortOrder, IsCollapsible, IsCheckedByDefault,
AddedOn
) VALUES (
:itemId, :bizId, 0, :parentId,
:name, '', 0, 1,

View file

@ -20,69 +20,74 @@ try {
uuid = beaconUUIDs[i];
// Check if beacon exists
qB = queryExecute("SELECT BeaconID FROM Beacons WHERE BeaconUUID = :uuid", { uuid: uuid }, { datasource: "payfrit" });
qB = queryExecute("SELECT ID FROM Beacons WHERE UUID = :uuid", { uuid: uuid }, { datasource: "payfrit" });
if (qB.recordCount == 0) {
queryExecute("INSERT INTO Beacons (BeaconUUID, BeaconBusinessID) VALUES (:uuid, :bizID)", { uuid: uuid, bizID: lazyDaisyID }, { datasource: "payfrit" });
queryExecute("INSERT INTO Beacons (UUID, BusinessID) VALUES (:uuid, :bizID)", { uuid: uuid, bizID: lazyDaisyID }, { datasource: "payfrit" });
qNew = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
beaconID = qNew.id;
response.steps.append("Created beacon " & beaconID & " with UUID: " & uuid);
} else {
beaconID = qB.BeaconID;
beaconID = qB.ID;
response.steps.append("Beacon exists: " & beaconID & " with UUID: " & uuid);
}
}
// Get service point Table 1
qSP = queryExecute("
SELECT ServicePointID FROM ServicePoints
WHERE ServicePointBusinessID = :bizID AND ServicePointName = 'Table 1'
SELECT ID FROM ServicePoints
WHERE BusinessID = :bizID AND Name = 'Table 1'
", { bizID: lazyDaisyID }, { datasource: "payfrit" });
if (qSP.recordCount == 0) {
queryExecute("
INSERT INTO ServicePoints (ServicePointBusinessID, ServicePointName, ServicePointTypeID)
VALUES (:bizID, 'Table 1', 1)
INSERT INTO ServicePoints (BusinessID, Name)
VALUES (:bizID, 'Table 1')
", { bizID: lazyDaisyID }, { datasource: "payfrit" });
qSP = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
servicePointID = qSP.id;
response.steps.append("Created service point 'Table 1' (ID: " & servicePointID & ")");
} else {
servicePointID = qSP.ServicePointID;
servicePointID = qSP.ID;
response.steps.append("Found service point 'Table 1' (ID: " & servicePointID & ")");
}
// Get all beacons and map them
qBeacons = queryExecute("SELECT BeaconID, BeaconUUID FROM Beacons", {}, { datasource: "payfrit" });
// Assign all beacons to the Table 1 service point
qBeacons = queryExecute("SELECT ID, UUID FROM Beacons WHERE BusinessID = :bizID", { bizID: lazyDaisyID }, { datasource: "payfrit" });
for (i = 1; i <= qBeacons.recordCount; i++) {
beaconID = qBeacons.BeaconID[i];
beaconID = qBeacons.ID[i];
// Delete old mapping if exists
queryExecute("DELETE FROM lt_Beacon_Businesses_ServicePoints WHERE BeaconID = :beaconID", { beaconID: beaconID }, { datasource: "payfrit" });
// Create new mapping
// Unassign this beacon from any existing service point
queryExecute("
INSERT INTO lt_Beacon_Businesses_ServicePoints (BeaconID, BusinessID, ServicePointID)
VALUES (:beaconID, :bizID, :spID)
UPDATE ServicePoints SET BeaconID = NULL, AssignedByUserID = NULL
WHERE BeaconID = :beaconID
", { beaconID: beaconID }, { datasource: "payfrit" });
// Assign beacon to Table 1 service point
queryExecute("
UPDATE ServicePoints SET BeaconID = :beaconID, AssignedByUserID = 1
WHERE ID = :spID AND BusinessID = :bizID
", { beaconID: beaconID, bizID: lazyDaisyID, spID: servicePointID }, { datasource: "payfrit" });
response.steps.append("Mapped beacon " & beaconID & " to Lazy Daisy, Table 1");
response.steps.append("Assigned beacon " & beaconID & " to Table 1");
}
// Get final status
qFinal = queryExecute("
SELECT lt.BeaconID, b.BeaconUUID, lt.BusinessID, biz.BusinessName, lt.ServicePointID, sp.ServicePointName
FROM lt_Beacon_Businesses_ServicePoints lt
JOIN Beacons b ON b.BeaconID = lt.BeaconID
JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
SELECT sp.ID AS ServicePointID, sp.BeaconID, sp.BusinessID,
b.Name AS BeaconName, b.UUID, sp.Name AS ServicePointName,
biz.Name AS BusinessName
FROM ServicePoints sp
JOIN Beacons b ON b.ID = sp.BeaconID
JOIN Businesses biz ON biz.ID = sp.BusinessID
WHERE sp.BeaconID IS NOT NULL
", {}, { datasource: "payfrit" });
beacons = [];
for (i = 1; i <= qFinal.recordCount; i++) {
arrayAppend(beacons, {
"BeaconID": qFinal.BeaconID[i],
"UUID": qFinal.BeaconUUID[i],
"UUID": qFinal.UUID[i],
"BusinessID": qFinal.BusinessID[i],
"BusinessName": qFinal.BusinessName[i],
"ServicePointID": qFinal.ServicePointID[i],

View file

@ -5,11 +5,11 @@ try {
// Create ChatMessages table
queryExecute("
CREATE TABLE IF NOT EXISTS ChatMessages (
MessageID INT AUTO_INCREMENT PRIMARY KEY,
ID INT AUTO_INCREMENT PRIMARY KEY,
TaskID INT NOT NULL,
SenderUserID INT NOT NULL,
SenderType ENUM('customer', 'worker') NOT NULL,
MessageText TEXT NOT NULL,
MessageBody TEXT NOT NULL,
IsRead TINYINT(1) DEFAULT 0,
CreatedOn DATETIME DEFAULT NOW(),
@ -21,13 +21,13 @@ try {
// Also add a "Chat" category if it doesn't exist for business 17
existing = queryExecute("
SELECT TaskCategoryID FROM TaskCategories
WHERE TaskCategoryBusinessID = 17 AND TaskCategoryName = 'Chat'
SELECT ID FROM TaskCategories
WHERE BusinessID = 17 AND Name = 'Chat'
", {}, { datasource: "payfrit" });
if (existing.recordCount == 0) {
queryExecute("
INSERT INTO TaskCategories (TaskCategoryBusinessID, TaskCategoryName, TaskCategoryColor)
INSERT INTO TaskCategories (BusinessID, Name, Color)
VALUES (17, 'Chat', '##2196F3')
", {}, { datasource: "payfrit" });
}

View file

@ -32,47 +32,47 @@ try {
queryExecute("
CREATE TABLE Menus (
MenuID INT AUTO_INCREMENT PRIMARY KEY,
MenuBusinessID INT NOT NULL,
MenuName VARCHAR(100) NOT NULL,
MenuDescription VARCHAR(500) NULL,
MenuDaysActive INT NOT NULL DEFAULT 127,
MenuStartTime TIME NULL,
MenuEndTime TIME NULL,
MenuSortOrder INT NOT NULL DEFAULT 0,
MenuIsActive TINYINT NOT NULL DEFAULT 1,
MenuAddedOn DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_menus_business (MenuBusinessID),
INDEX idx_menus_active (MenuBusinessID, MenuIsActive)
BusinessID INT NOT NULL,
Name VARCHAR(100) NOT NULL,
Description VARCHAR(500) NULL,
DaysActive INT NOT NULL DEFAULT 127,
StartTime TIME NULL,
EndTime TIME NULL,
SortOrder INT NOT NULL DEFAULT 0,
IsActive TINYINT NOT NULL DEFAULT 1,
AddedOn DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_menus_business (BusinessID),
INDEX idx_menus_active (BusinessID, IsActive)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
", {}, { datasource: "payfrit" });
response["OK"] = true;
response["MESSAGE"] = "Menus table created successfully";
response["SCHEMA"] = {
"MenuDaysActive": "Bitmask: 1=Sun, 2=Mon, 4=Tue, 8=Wed, 16=Thu, 32=Fri, 64=Sat (127 = all days)"
"DaysActive": "Bitmask: 1=Sun, 2=Mon, 4=Tue, 8=Wed, 16=Thu, 32=Fri, 64=Sat (127 = all days)"
};
}
// Check if CategoryMenuID column exists in Categories table
// Check if MenuID column exists in Categories table
qCatCol = queryExecute("
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'payfrit'
AND TABLE_NAME = 'Categories'
AND COLUMN_NAME = 'CategoryMenuID'
AND COLUMN_NAME = 'MenuID'
", {}, { datasource: "payfrit" });
if (qCatCol.recordCount == 0) {
// Add CategoryMenuID column to Categories table
// Add MenuID column to Categories table
queryExecute("
ALTER TABLE Categories
ADD COLUMN CategoryMenuID INT NULL DEFAULT NULL AFTER CategoryBusinessID,
ADD INDEX idx_categories_menu (CategoryMenuID)
ADD COLUMN MenuID INT NULL DEFAULT NULL AFTER BusinessID,
ADD INDEX idx_categories_menu (MenuID)
", {}, { datasource: "payfrit" });
response["CATEGORIES_UPDATED"] = true;
response["CATEGORIES_MESSAGE"] = "Added CategoryMenuID column to Categories table";
response["CATEGORIES_MESSAGE"] = "Added MenuID column to Categories table";
} else {
response["CATEGORIES_UPDATED"] = false;
response["CATEGORIES_MESSAGE"] = "CategoryMenuID column already exists";
response["CATEGORIES_MESSAGE"] = "MenuID column already exists";
}
} catch (any e) {

View file

@ -12,7 +12,7 @@
*
* POST body:
* {
* "BusinessName": "Century Casino",
* "Name": "Century Casino",
* "UserID": 1,
* "ChildBusinessIDs": [47, 48] // Optional: link existing businesses as children
* }
@ -36,13 +36,13 @@ response = { "OK": false };
try {
data = readJsonBody();
BusinessName = structKeyExists(data, "BusinessName") ? trim(data.BusinessName) : "";
Name = structKeyExists(data, "Name") ? trim(data.Name) : "";
UserID = structKeyExists(data, "UserID") ? val(data.UserID) : 0;
ChildBusinessIDs = structKeyExists(data, "ChildBusinessIDs") && isArray(data.ChildBusinessIDs) ? data.ChildBusinessIDs : [];
if (!len(BusinessName)) {
if (!len(Name)) {
response["ERROR"] = "missing_name";
response["MESSAGE"] = "BusinessName is required";
response["MESSAGE"] = "Name is required";
writeOutput(serializeJSON(response));
abort;
}
@ -56,7 +56,7 @@ try {
// Create minimal address record (just a placeholder)
queryExecute("
INSERT INTO Addresses (AddressLine1, AddressUserID, AddressTypeID, AddressAddedOn)
INSERT INTO Addresses (Line1, UserID, AddressTypeID, AddedOn)
VALUES ('Parent Business - No Physical Location', :userID, 2, NOW())
", {
userID: UserID
@ -67,10 +67,10 @@ try {
// Create parent business (no menu, no hours, just a shell)
queryExecute("
INSERT INTO Businesses (BusinessName, BusinessUserID, BusinessAddressID, BusinessParentBusinessID, BusinessDeliveryZipCodes, BusinessAddedOn)
INSERT INTO Businesses (Name, UserID, AddressID, ParentBusinessID, BusinessDeliveryZipCodes, AddedOn)
VALUES (:name, :userId, :addressId, NULL, '', NOW())
", {
name: BusinessName,
name: Name,
userId: UserID,
addressId: addressId
}, { datasource = "payfrit" });
@ -80,7 +80,7 @@ try {
// Link address back to business
queryExecute("
UPDATE Addresses SET AddressBusinessID = :bizId WHERE AddressID = :addrId
UPDATE Addresses SET BusinessID = :bizId WHERE ID = :addrId
", {
bizId: newBusinessID,
addrId: addressId
@ -92,7 +92,7 @@ try {
childID = val(childID);
if (childID > 0) {
queryExecute("
UPDATE Businesses SET BusinessParentBusinessID = :parentId WHERE BusinessID = :childId
UPDATE Businesses SET ParentBusinessID = :parentId WHERE ID = :childId
", {
parentId: newBusinessID,
childId: childID
@ -103,7 +103,7 @@ try {
response["OK"] = true;
response["BusinessID"] = newBusinessID;
response["BusinessName"] = BusinessName;
response["Name"] = Name;
response["MESSAGE"] = "Parent business created";
if (arrayLen(linkedChildren) > 0) {
response["LinkedChildren"] = linkedChildren;

View file

@ -9,22 +9,22 @@ bizId = 27;
deactivatedIds = [11177, 11180, 11183, 11186, 11190, 11193, 11196, 11199, 11204, 11212, 11220, 11259];
qDeactivated = queryExecute("
SELECT i.ItemID, i.ItemName, i.ItemParentItemID, i.ItemIsActive, i.ItemIsCollapsible,
(SELECT COUNT(*) FROM Items c WHERE c.ItemParentItemID = i.ItemID) as ChildCount,
(SELECT GROUP_CONCAT(c.ItemName) FROM Items c WHERE c.ItemParentItemID = i.ItemID) as Children
SELECT i.ID, i.Name, i.ParentItemID, i.IsActive, i.IsCollapsible,
(SELECT COUNT(*) FROM Items c WHERE c.ParentItemID = i.ID) as ChildCount,
(SELECT GROUP_CONCAT(c.Name) FROM Items c WHERE c.ParentItemID = i.ID) as Children
FROM Items i
WHERE i.ItemID IN (:ids)
ORDER BY i.ItemID
WHERE i.ID IN (:ids)
ORDER BY i.ID
", { ids: { value: arrayToList(deactivatedIds), list: true } }, { datasource: "payfrit" });
items = [];
for (row in qDeactivated) {
arrayAppend(items, {
"ItemID": row.ItemID,
"ItemName": row.ItemName,
"ParentID": row.ItemParentItemID,
"IsActive": row.ItemIsActive,
"IsCollapsible": row.ItemIsCollapsible,
"ItemID": row.ID,
"Name": row.Name,
"ParentID": row.ParentItemID,
"IsActive": row.IsActive,
"IsCollapsible": row.IsCollapsible,
"ChildCount": row.ChildCount,
"Children": row.Children
});

View file

@ -9,24 +9,24 @@ bizId = 27;
qLinks = queryExecute("
SELECT
tl.ItemID as MenuItemID,
mi.ItemName as MenuItemName,
mi.ItemParentItemID,
mi.Name as MenuName,
mi.ParentItemID,
tl.TemplateItemID,
t.ItemName as TemplateName,
t.Name as TemplateName,
tl.SortOrder
FROM ItemTemplateLinks tl
JOIN Items mi ON mi.ItemID = tl.ItemID
FROM lt_ItemID_TemplateItemID tl
JOIN Items mi ON mi.ID = tl.ItemID
JOIN Items t ON t.ItemID = tl.TemplateItemID
WHERE mi.ItemBusinessID = :bizId
ORDER BY mi.ItemParentItemID, mi.ItemName, tl.SortOrder
WHERE mi.BusinessID = :bizId
ORDER BY mi.ParentItemID, mi.Name, tl.SortOrder
", { bizId: bizId }, { datasource: "payfrit" });
links = [];
for (row in qLinks) {
arrayAppend(links, {
"MenuItemID": row.MenuItemID,
"MenuItemName": row.MenuItemName,
"ParentItemID": row.ItemParentItemID,
"MenuName": row.MenuName,
"ParentItemID": row.ParentItemID,
"TemplateItemID": row.TemplateItemID,
"TemplateName": row.TemplateName
});
@ -34,20 +34,20 @@ for (row in qLinks) {
// Get burgers specifically (parent = 11271)
qBurgers = queryExecute("
SELECT ItemID, ItemName FROM Items
WHERE ItemBusinessID = :bizId AND ItemParentItemID = 11271 AND ItemIsActive = 1
ORDER BY ItemSortOrder
SELECT ID, Name FROM Items
WHERE BusinessID = :bizId AND ParentItemID = 11271 AND IsActive = 1
ORDER BY SortOrder
", { bizId: bizId }, { datasource: "payfrit" });
burgers = [];
for (row in qBurgers) {
// Get templates for this burger
qBurgerTemplates = queryExecute("
SELECT tl.TemplateItemID, t.ItemName as TemplateName
FROM ItemTemplateLinks tl
SELECT tl.TemplateItemID, t.Name as TemplateName
FROM lt_ItemID_TemplateItemID tl
JOIN Items t ON t.ItemID = tl.TemplateItemID
WHERE tl.ItemID = :itemId
", { itemId: row.ItemID }, { datasource: "payfrit" });
", { itemId: row.ID }, { datasource: "payfrit" });
templates = [];
for (t in qBurgerTemplates) {
@ -55,8 +55,8 @@ for (row in qBurgers) {
}
arrayAppend(burgers, {
"ItemID": row.ItemID,
"ItemName": row.ItemName,
"ItemID": row.ID,
"Name": row.Name,
"Templates": templates
});
}

View file

@ -9,38 +9,38 @@ businessID = 27;
qCategories = queryExecute("
SELECT DISTINCT
p.ItemID as CategoryID,
p.ItemName as CategoryName,
p.ItemSortOrder
p.Name as Name,
p.SortOrder
FROM Items p
INNER JOIN Items c ON c.ItemParentItemID = p.ItemID
WHERE p.ItemBusinessID = :businessID
AND p.ItemParentItemID = 0
AND p.ItemIsActive = 1
INNER JOIN Items c ON c.ParentItemID = p.ItemID
WHERE p.BusinessID = :businessID
AND p.ParentItemID = 0
AND p.IsActive = 1
AND NOT EXISTS (
SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = p.ItemID
SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = p.ItemID
)
ORDER BY p.ItemSortOrder, p.ItemName
ORDER BY p.SortOrder, p.Name
", { businessID: businessID });
cats = [];
for (c in qCategories) {
arrayAppend(cats, {
"CategoryID": c.CategoryID,
"CategoryName": c.CategoryName
"CategoryID": c.ID,
"Name": c.Name
});
}
// Also check raw counts
rawCount = queryExecute("
SELECT COUNT(*) as cnt FROM Items
WHERE ItemBusinessID = :bizId AND ItemParentItemID = 0 AND ItemIsActive = 1
WHERE BusinessID = :bizId AND ParentItemID = 0 AND IsActive = 1
", { bizId: businessID });
childrenCount = queryExecute("
SELECT COUNT(DISTINCT c.ItemParentItemID) as cnt
SELECT COUNT(DISTINCT c.ParentItemID) as cnt
FROM Items c
INNER JOIN Items p ON p.ItemID = c.ItemParentItemID
WHERE p.ItemBusinessID = :bizId AND p.ItemParentItemID = 0
INNER JOIN Items p ON p.ItemID = c.ParentItemID
WHERE p.BusinessID = :bizId AND p.ParentItemID = 0
", { bizId: businessID });
writeOutput(serializeJSON({

View file

@ -9,24 +9,24 @@ bizId = 27;
qLinks = queryExecute("
SELECT
tl.ItemID as MenuItemID,
mi.ItemName as MenuItemName,
mi.ItemParentItemID as MenuItemParentID,
mi.Name as MenuName,
mi.ParentItemID as MenuItemParentID,
tl.TemplateItemID,
t.ItemName as TemplateName,
t.ItemIsActive as TemplateActive,
t.Name as TemplateName,
t.IsActive as TemplateActive,
tl.SortOrder
FROM ItemTemplateLinks tl
JOIN Items mi ON mi.ItemID = tl.ItemID
FROM lt_ItemID_TemplateItemID tl
JOIN Items mi ON mi.ID = tl.ItemID
JOIN Items t ON t.ItemID = tl.TemplateItemID
WHERE mi.ItemBusinessID = :bizId
ORDER BY mi.ItemName, tl.SortOrder
WHERE mi.BusinessID = :bizId
ORDER BY mi.Name, tl.SortOrder
", { bizId: bizId }, { datasource: "payfrit" });
links = [];
for (row in qLinks) {
arrayAppend(links, {
"MenuItemID": row.MenuItemID,
"MenuItemName": row.MenuItemName,
"MenuName": row.MenuName,
"MenuItemParentID": row.MenuItemParentID,
"TemplateItemID": row.TemplateItemID,
"TemplateName": row.TemplateName,
@ -37,20 +37,20 @@ for (row in qLinks) {
// Get all templates that exist for this business
qTemplates = queryExecute("
SELECT ItemID, ItemName, ItemIsActive, ItemParentItemID
SELECT ID, Name, IsActive, ParentItemID
FROM Items
WHERE ItemBusinessID = :bizId
AND ItemIsCollapsible = 1
ORDER BY ItemName
WHERE BusinessID = :bizId
AND IsCollapsible = 1
ORDER BY Name
", { bizId: bizId }, { datasource: "payfrit" });
templates = [];
for (row in qTemplates) {
arrayAppend(templates, {
"ItemID": row.ItemID,
"ItemName": row.ItemName,
"IsActive": row.ItemIsActive,
"ParentID": row.ItemParentItemID
"ItemID": row.ID,
"Name": row.Name,
"IsActive": row.IsActive,
"ParentID": row.ParentItemID
});
}

View file

@ -5,67 +5,67 @@
<cfscript>
bizId = 27;
// Check the template items themselves (IDs from ItemTemplateLinks)
// Check the template items themselves (IDs from lt_ItemID_TemplateItemID)
templateIds = "11267, 11251, 11246, 11224, 11233, 11230, 11240, 11243, 11237, 11227";
qTemplates = queryExecute("
SELECT ItemID, ItemName, ItemIsCollapsible, ItemIsActive, ItemParentItemID, ItemBusinessID
SELECT ID, Name, IsCollapsible, IsActive, ParentItemID, BusinessID
FROM Items
WHERE ItemID IN (#templateIds#)
ORDER BY ItemName
WHERE ID IN (#templateIds#)
ORDER BY Name
", {}, { datasource: "payfrit" });
templates = [];
for (row in qTemplates) {
arrayAppend(templates, {
"ItemID": row.ItemID,
"ItemName": row.ItemName,
"IsCollapsible": row.ItemIsCollapsible,
"IsActive": row.ItemIsActive,
"ParentID": row.ItemParentItemID,
"BusinessID": row.ItemBusinessID
"ItemID": row.ID,
"Name": row.Name,
"IsCollapsible": row.IsCollapsible,
"IsActive": row.IsActive,
"ParentID": row.ParentItemID,
"BusinessID": row.BusinessID
});
}
// Also check what other templates might exist for burgers
// Look for items that are in ItemTemplateLinks but NOT linked to burgers
// Look for items that are in lt_ItemID_TemplateItemID but NOT linked to burgers
qMissingTemplates = queryExecute("
SELECT DISTINCT t.ItemID, t.ItemName, t.ItemIsCollapsible, t.ItemIsActive
SELECT DISTINCT t.ItemID, t.Name, t.IsCollapsible, t.IsActive
FROM Items t
WHERE t.ItemBusinessID = :bizId
AND t.ItemParentItemID = 0
WHERE t.BusinessID = :bizId
AND t.ParentItemID = 0
AND t.ItemID NOT IN (
SELECT i.ItemID FROM Items i WHERE i.ItemBusinessID = :bizId AND i.ItemCategoryID > 0
SELECT i.ID FROM Items i WHERE i.BusinessID = :bizId AND i.CategoryID > 0
)
AND EXISTS (SELECT 1 FROM Items child WHERE child.ItemParentItemID = t.ItemID)
ORDER BY t.ItemName
AND EXISTS (SELECT 1 FROM Items child WHERE child.ParentItemID = t.ItemID)
ORDER BY t.Name
", { bizId: bizId }, { datasource: "payfrit" });
potentialTemplates = [];
for (row in qMissingTemplates) {
arrayAppend(potentialTemplates, {
"ItemID": row.ItemID,
"ItemName": row.ItemName,
"IsCollapsible": row.ItemIsCollapsible,
"IsActive": row.ItemIsActive
"ItemID": row.ID,
"Name": row.Name,
"IsCollapsible": row.IsCollapsible,
"IsActive": row.IsActive
});
}
// What templates SHOULD burgers have? Let's see all templates used by ANY item
qAllTemplateUsage = queryExecute("
SELECT t.ItemID, t.ItemName, COUNT(tl.ItemID) as UsageCount
FROM ItemTemplateLinks tl
SELECT t.ItemID, t.Name, COUNT(tl.ItemID) as UsageCount
FROM lt_ItemID_TemplateItemID tl
JOIN Items t ON t.ItemID = tl.TemplateItemID
JOIN Items mi ON mi.ItemID = tl.ItemID AND mi.ItemBusinessID = :bizId
GROUP BY t.ItemID, t.ItemName
ORDER BY t.ItemName
JOIN Items mi ON mi.ID = tl.ItemID AND mi.BusinessID = :bizId
GROUP BY t.ItemID, t.Name
ORDER BY t.Name
", { bizId: bizId }, { datasource: "payfrit" });
allTemplates = [];
for (row in qAllTemplateUsage) {
arrayAppend(allTemplates, {
"TemplateID": row.ItemID,
"TemplateName": row.ItemName,
"TemplateID": row.ID,
"TemplateName": row.Name,
"UsageCount": row.UsageCount
});
}

View file

@ -7,41 +7,41 @@ bizId = 27;
// Get the template items themselves
qTemplates = queryExecute("
SELECT ItemID, ItemName, ItemIsCollapsible, ItemIsActive, ItemParentItemID, ItemBusinessID
SELECT ID, Name, IsCollapsible, IsActive, ParentItemID, BusinessID
FROM Items
WHERE ItemID IN (11267, 11251, 11246, 11224, 11233, 11230, 11240, 11243, 11237, 11227)
ORDER BY ItemName
WHERE ID IN (11267, 11251, 11246, 11224, 11233, 11230, 11240, 11243, 11237, 11227)
ORDER BY Name
", {}, { datasource: "payfrit" });
templates = [];
for (row in qTemplates) {
arrayAppend(templates, {
"ItemID": row.ItemID,
"ItemName": row.ItemName,
"IsCollapsible": row.ItemIsCollapsible,
"IsActive": row.ItemIsActive,
"ParentID": row.ItemParentItemID,
"BusinessID": row.ItemBusinessID
"ItemID": row.ID,
"Name": row.Name,
"IsCollapsible": row.IsCollapsible,
"IsActive": row.IsActive,
"ParentID": row.ParentItemID,
"BusinessID": row.BusinessID
});
}
// What templates are used by burgers vs all items?
qBurgerLinks = queryExecute("
SELECT mi.ItemID, mi.ItemName, GROUP_CONCAT(t.ItemName ORDER BY tl.SortOrder) as Templates
SELECT mi.ID, mi.Name, GROUP_CONCAT(t.Name ORDER BY tl.SortOrder) as Templates
FROM Items mi
JOIN ItemTemplateLinks tl ON tl.ItemID = mi.ItemID
JOIN lt_ItemID_TemplateItemID tl ON tl.ItemID = mi.ID
JOIN Items t ON t.ItemID = tl.TemplateItemID
WHERE mi.ItemBusinessID = :bizId
AND mi.ItemParentItemID = 11271
GROUP BY mi.ItemID, mi.ItemName
ORDER BY mi.ItemName
WHERE mi.BusinessID = :bizId
AND mi.ParentItemID = 11271
GROUP BY mi.ID, mi.Name
ORDER BY mi.Name
", { bizId: bizId }, { datasource: "payfrit" });
burgerLinks = [];
for (row in qBurgerLinks) {
arrayAppend(burgerLinks, {
"ItemID": row.ItemID,
"ItemName": row.ItemName,
"ItemID": row.ID,
"Name": row.Name,
"Templates": row.Templates
});
}
@ -49,20 +49,20 @@ for (row in qBurgerLinks) {
// Also check: are there templates that SHOULD be linked to burgers?
// (e.g., Add Cheese, etc.)
qCheeseTemplate = queryExecute("
SELECT ItemID, ItemName, ItemParentItemID, ItemIsActive
SELECT ID, Name, ParentItemID, IsActive
FROM Items
WHERE ItemBusinessID = :bizId
AND ItemName LIKE '%Cheese%'
ORDER BY ItemName
WHERE BusinessID = :bizId
AND Name LIKE '%Cheese%'
ORDER BY Name
", { bizId: bizId }, { datasource: "payfrit" });
cheeseItems = [];
for (row in qCheeseTemplate) {
arrayAppend(cheeseItems, {
"ItemID": row.ItemID,
"ItemName": row.ItemName,
"ParentID": row.ItemParentItemID,
"IsActive": row.ItemIsActive
"ItemID": row.ID,
"Name": row.Name,
"ParentID": row.ParentItemID,
"IsActive": row.IsActive
});
}

View file

@ -26,36 +26,36 @@
<cftry>
<!--- Get raw employee records --- >
<cfset qEmployees = queryExecute("
SELECT e.*, b.BusinessName
FROM lt_Users_Businesses_Employees e
INNER JOIN Businesses b ON b.BusinessID = e.BusinessID
SELECT e.*, b.Name
FROM Employees e
INNER JOIN Businesses b ON b.ID = e.BusinessID
WHERE e.UserID = ?
ORDER BY b.BusinessName ASC
ORDER BY b.Name ASC
", [ { value = UserID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<cfset employees = []>
<cfloop query="qEmployees">
<cfset arrayAppend(employees, {
"EmployeeID": qEmployees.EmployeeID,
"EmployeeID": qEmployees.ID,
"UserID": qEmployees.UserID,
"BusinessID": qEmployees.BusinessID,
"BusinessName": qEmployees.BusinessName,
"EmployeeIsActive": qEmployees.EmployeeIsActive
"Name": qEmployees.Name,
"IsActive": qEmployees.IsActive
})>
</cfloop>
<!--- Check for duplicate businesses --- >
<cfset qDuplicates = queryExecute("
SELECT BusinessName, COUNT(*) AS cnt
SELECT Name, COUNT(*) AS cnt
FROM Businesses
GROUP BY BusinessName
GROUP BY Name
HAVING COUNT(*) > 1
", [], { datasource = "payfrit" })>
<cfset duplicates = []>
<cfloop query="qDuplicates">
<cfset arrayAppend(duplicates, {
"BusinessName": qDuplicates.BusinessName,
"Name": qDuplicates.Name,
"Count": qDuplicates.cnt
})>
</cfloop>

View file

@ -10,11 +10,11 @@ try {
messages = [];
for (row in qAll) {
arrayAppend(messages, {
"MessageID": row.MessageID,
"MessageID": row.ID,
"TaskID": row.TaskID,
"SenderUserID": row.SenderUserID,
"SenderType": row.SenderType,
"MessageText": left(row.MessageText, 100),
"MessageBody": left(row.MessageBody, 100),
"CreatedOn": row.CreatedOn
});
}

View file

@ -10,54 +10,54 @@ response = { "OK": true };
try {
// Get Fountain Drinks item
qFountain = queryExecute("
SELECT ItemID, ItemName, ItemParentItemID, ItemPrice, ItemIsCollapsible, ItemRequiresChildSelection
SELECT ID, Name, ParentItemID, Price, IsCollapsible, RequiresChildSelection
FROM Items
WHERE ItemBusinessID = 17 AND ItemName LIKE '%Fountain%'
WHERE BusinessID = 17 AND Name LIKE '%Fountain%'
", {}, { datasource: "payfrit" });
response["FountainDrinks"] = [];
for (row in qFountain) {
fountainItem = {
"ItemID": row.ItemID,
"ItemName": row.ItemName,
"ItemPrice": row.ItemPrice,
"ItemIsCollapsible": row.ItemIsCollapsible,
"ItemRequiresChildSelection": row.ItemRequiresChildSelection,
"ItemID": row.ID,
"Name": row.Name,
"Price": row.Price,
"IsCollapsible": row.IsCollapsible,
"RequiresChildSelection": row.RequiresChildSelection,
"Children": []
};
// Get children of this item
qChildren = queryExecute("
SELECT ItemID, ItemName, ItemParentItemID, ItemPrice, ItemIsCollapsible, ItemRequiresChildSelection, ItemIsCheckedByDefault
SELECT ID, Name, ParentItemID, Price, IsCollapsible, RequiresChildSelection, IsCheckedByDefault
FROM Items
WHERE ItemParentItemID = :parentId
ORDER BY ItemSortOrder
", { parentId: row.ItemID }, { datasource: "payfrit" });
WHERE ParentItemID = :parentId
ORDER BY SortOrder
", { parentId: row.ID }, { datasource: "payfrit" });
for (child in qChildren) {
childItem = {
"ItemID": child.ItemID,
"ItemName": child.ItemName,
"ItemPrice": child.ItemPrice,
"ItemIsCollapsible": child.ItemIsCollapsible,
"ItemIsCheckedByDefault": child.ItemIsCheckedByDefault,
"Name": child.Name,
"Price": child.Price,
"IsCollapsible": child.IsCollapsible,
"IsCheckedByDefault": child.IsCheckedByDefault,
"Grandchildren": []
};
// Get grandchildren
qGrandchildren = queryExecute("
SELECT ItemID, ItemName, ItemPrice, ItemIsCheckedByDefault
SELECT ID, Name, Price, IsCheckedByDefault
FROM Items
WHERE ItemParentItemID = :parentId
ORDER BY ItemSortOrder
WHERE ParentItemID = :parentId
ORDER BY SortOrder
", { parentId: child.ItemID }, { datasource: "payfrit" });
for (gc in qGrandchildren) {
arrayAppend(childItem.Grandchildren, {
"ItemID": gc.ItemID,
"ItemName": gc.ItemName,
"ItemPrice": gc.ItemPrice,
"ItemIsCheckedByDefault": gc.ItemIsCheckedByDefault
"Name": gc.Name,
"Price": gc.Price,
"IsCheckedByDefault": gc.IsCheckedByDefault
});
}

View file

@ -14,10 +14,10 @@ if (structKeyExists(data, "Phone") && len(data.Phone)) {
phone = reReplace(data.Phone, "[^0-9]", "", "all");
qUser = queryExecute("
SELECT UserID, UserFirstName, UserLastName, UserEmailAddress, UserContactNumber
SELECT ID, FirstName, LastName, EmailAddress, ContactNumber
FROM Users
WHERE REPLACE(REPLACE(REPLACE(UserContactNumber, '-', ''), '(', ''), ')', '') LIKE ?
OR UserContactNumber LIKE ?
WHERE REPLACE(REPLACE(REPLACE(ContactNumber, '-', ''), '(', ''), ')', '') LIKE ?
OR ContactNumber LIKE ?
", [
{ value: "%" & phone & "%", cfsqltype: "cf_sql_varchar" },
{ value: "%" & phone & "%", cfsqltype: "cf_sql_varchar" }
@ -28,35 +28,35 @@ if (structKeyExists(data, "Phone") && len(data.Phone)) {
abort;
}
userId = qUser.UserID;
userId = qUser.ID;
qEmployees = queryExecute("
SELECT e.EmployeeID, e.BusinessID, e.EmployeeStatusID,
CAST(e.EmployeeIsActive AS UNSIGNED) AS EmployeeIsActive,
b.BusinessName
FROM lt_Users_Businesses_Employees e
JOIN Businesses b ON e.BusinessID = b.BusinessID
SELECT e.ID, e.BusinessID, e.StatusID,
CAST(e.IsActive AS UNSIGNED) AS IsActive,
b.Name
FROM Employees e
JOIN Businesses b ON e.BusinessID = b.ID
WHERE e.UserID = ?
", [{ value: userId, cfsqltype: "cf_sql_integer" }], { datasource: "payfrit" });
employees = [];
for (row in qEmployees) {
arrayAppend(employees, {
"EmployeeID": row.EmployeeID,
"EmployeeID": row.ID,
"BusinessID": row.BusinessID,
"BusinessName": row.BusinessName,
"StatusID": row.EmployeeStatusID,
"IsActive": row.EmployeeIsActive
"Name": row.Name,
"StatusID": row.StatusID,
"IsActive": row.IsActive
});
}
writeOutput(serializeJSON({
"OK": true,
"USER": {
"UserID": qUser.UserID,
"Name": trim(qUser.UserFirstName & " " & qUser.UserLastName),
"Email": qUser.UserEmailAddress,
"Phone": qUser.UserContactNumber
"UserID": qUser.ID,
"Name": trim(qUser.FirstName & " " & qUser.LastName),
"Email": qUser.EmailAddress,
"Phone": qUser.ContactNumber
},
"EMPLOYEES": employees
}));
@ -67,23 +67,23 @@ if (structKeyExists(data, "Phone") && len(data.Phone)) {
businessId = structKeyExists(data, "BusinessID") ? val(data.BusinessID) : 17;
q = queryExecute("
SELECT EmployeeID, UserID, EmployeeStatusID, EmployeeIsActive,
CAST(EmployeeIsActive AS UNSIGNED) AS IsActiveInt
FROM lt_Users_Businesses_Employees
SELECT ID, UserID, StatusID, IsActive,
CAST(IsActive AS UNSIGNED) AS IsActiveInt
FROM Employees
WHERE BusinessID = ?
", [{ value: businessId, cfsqltype: "cf_sql_integer" }], { datasource: "payfrit" });
rows = [];
for (r in q) {
arrayAppend(rows, {
"EmployeeID": r.EmployeeID,
"EmployeeID": r.ID,
"UserID": r.UserID,
"StatusID": r.EmployeeStatusID,
"RawIsActive": r.EmployeeIsActive,
"StatusID": r.StatusID,
"RawIsActive": r.IsActive,
"CastIsActive": r.IsActiveInt,
"ValRaw": val(r.EmployeeIsActive),
"ValRaw": val(r.IsActive),
"ValCast": val(r.IsActiveInt),
"EqRaw1": r.EmployeeIsActive == 1,
"EqRaw1": r.IsActive == 1,
"EqCast1": r.IsActiveInt == 1
});
}

View file

@ -13,8 +13,8 @@ try {
// Close all open chats action
if (structKeyExists(data, "action") && data.action == "closeAllChats") {
queryExecute("
UPDATE Tasks SET TaskCompletedOn = NOW()
WHERE TaskTypeID = 2 AND TaskCompletedOn IS NULL
UPDATE Tasks SET CompletedOn = NOW()
WHERE TaskTypeID = 2 AND CompletedOn IS NULL
", {}, { datasource: "payfrit" });
writeOutput(serializeJSON({ "OK": true, "MESSAGE": "All open chats closed" }));
abort;
@ -24,38 +24,38 @@ if (structKeyExists(data, "action") && data.action == "closeAllChats") {
<cftry>
<cfset qTasks = queryExecute("
SELECT
t.TaskID,
t.TaskBusinessID,
t.TaskOrderID,
t.TaskClaimedByUserID,
t.TaskClaimedOn,
t.TaskCompletedOn,
o.OrderStatusID
t.ID,
t.BusinessID,
t.OrderID,
t.ClaimedByUserID,
t.ClaimedOn,
t.CompletedOn,
o.StatusID
FROM Tasks t
LEFT JOIN Orders o ON o.OrderID = t.TaskOrderID
ORDER BY t.TaskID DESC
LEFT JOIN Orders o ON o.ID = t.OrderID
ORDER BY t.ID DESC
LIMIT 20
", [], { datasource = "payfrit" })>
<cfset tasks = []>
<cfloop query="qTasks">
<cfset arrayAppend(tasks, {
"TaskID": qTasks.TaskID,
"TaskBusinessID": qTasks.TaskBusinessID,
"TaskOrderID": qTasks.TaskOrderID,
"TaskClaimedByUserID": qTasks.TaskClaimedByUserID,
"TaskClaimedOn": isNull(qTasks.TaskClaimedOn) ? "NULL" : dateTimeFormat(qTasks.TaskClaimedOn, "yyyy-mm-dd HH:nn:ss"),
"TaskCompletedOn": isNull(qTasks.TaskCompletedOn) ? "NULL" : dateTimeFormat(qTasks.TaskCompletedOn, "yyyy-mm-dd HH:nn:ss"),
"OrderStatusID": isNull(qTasks.OrderStatusID) ? "NULL" : qTasks.OrderStatusID
"TaskID": qTasks.ID,
"BusinessID": qTasks.BusinessID,
"OrderID": qTasks.OrderID,
"ClaimedByUserID": qTasks.ClaimedByUserID,
"ClaimedOn": isNull(qTasks.ClaimedOn) ? "NULL" : dateTimeFormat(qTasks.ClaimedOn, "yyyy-mm-dd HH:nn:ss"),
"CompletedOn": isNull(qTasks.CompletedOn) ? "NULL" : dateTimeFormat(qTasks.CompletedOn, "yyyy-mm-dd HH:nn:ss"),
"StatusID": isNull(qTasks.StatusID) ? "NULL" : qTasks.StatusID
})>
</cfloop>
<cfset qStats = queryExecute("
SELECT
COUNT(*) as Total,
SUM(CASE WHEN TaskClaimedByUserID > 0 AND TaskCompletedOn IS NULL THEN 1 ELSE 0 END) as ClaimedNotCompleted,
SUM(CASE WHEN TaskClaimedByUserID = 0 OR TaskClaimedByUserID IS NULL THEN 1 ELSE 0 END) as Unclaimed,
SUM(CASE WHEN TaskCompletedOn IS NOT NULL THEN 1 ELSE 0 END) as Completed
SUM(CASE WHEN ClaimedByUserID > 0 AND CompletedOn IS NULL THEN 1 ELSE 0 END) as ClaimedNotCompleted,
SUM(CASE WHEN ClaimedByUserID = 0 OR ClaimedByUserID IS NULL THEN 1 ELSE 0 END) as Unclaimed,
SUM(CASE WHEN CompletedOn IS NOT NULL THEN 1 ELSE 0 END) as Completed
FROM Tasks
", [], { datasource = "payfrit" })>

View file

@ -4,19 +4,19 @@
<cftry>
<cfset qAll = queryExecute("
SELECT TaskID, TaskClaimedByUserID, TaskCompletedOn, TaskOrderID,
CASE WHEN TaskCompletedOn IS NULL THEN 'YES_NULL' ELSE 'NOT_NULL' END AS IsNull
SELECT ID, ClaimedByUserID, CompletedOn, OrderID,
CASE WHEN CompletedOn IS NULL THEN 'YES_NULL' ELSE 'NOT_NULL' END AS IsNull
FROM Tasks
ORDER BY TaskID DESC
ORDER BY ID DESC
", [], { datasource = "payfrit" })>
<cfset tasks = []>
<cfloop query="qAll">
<cfset arrayAppend(tasks, {
"TaskID": qAll.TaskID,
"TaskClaimedByUserID": qAll.TaskClaimedByUserID,
"TaskOrderID": qAll.TaskOrderID,
"TaskCompletedOn": len(trim(qAll.TaskCompletedOn)) ? toString(qAll.TaskCompletedOn) : "",
"TaskID": qAll.ID,
"ClaimedByUserID": qAll.ClaimedByUserID,
"OrderID": qAll.OrderID,
"CompletedOn": len(trim(qAll.CompletedOn)) ? toString(qAll.CompletedOn) : "",
"IsNull": qAll.IsNull
})>
</cfloop>

View file

@ -3,39 +3,39 @@
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
// Check ItemTemplateLinks for Big Dean's (BusinessID 27)
// Check lt_ItemID_TemplateItemID for Big Dean's (BusinessID 27)
bizId = 27;
// Count total links
qCount = queryExecute("SELECT COUNT(*) as cnt FROM ItemTemplateLinks", {}, { datasource: "payfrit" });
qCount = queryExecute("SELECT COUNT(*) as cnt FROM lt_ItemID_TemplateItemID", {}, { datasource: "payfrit" });
// Get template item IDs for this business
qTemplates = queryExecute("
SELECT DISTINCT tl.TemplateItemID, i.ItemName
FROM ItemTemplateLinks tl
JOIN Items i ON i.ItemID = tl.TemplateItemID
WHERE i.ItemBusinessID = :bizId
SELECT DISTINCT tl.TemplateItemID, i.Name
FROM lt_ItemID_TemplateItemID tl
JOIN Items i ON i.ID = tl.TemplateItemID
WHERE i.BusinessID = :bizId
", { bizId: bizId }, { datasource: "payfrit" });
templates = [];
for (row in qTemplates) {
arrayAppend(templates, { "TemplateItemID": row.TemplateItemID, "ItemName": row.ItemName });
arrayAppend(templates, { "TemplateItemID": row.TemplateItemID, "Name": row.Name });
}
// Get items that should be categories (ParentItemID=0, not templates)
qCategories = queryExecute("
SELECT i.ItemID, i.ItemName, i.ItemParentItemID, i.ItemIsCollapsible
SELECT i.ID, i.Name, i.ParentItemID, i.IsCollapsible
FROM Items i
WHERE i.ItemBusinessID = :bizId
AND i.ItemParentItemID = 0
AND i.ItemIsCollapsible = 0
AND NOT EXISTS (SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = i.ItemID)
ORDER BY i.ItemSortOrder
WHERE i.BusinessID = :bizId
AND i.ParentItemID = 0
AND i.IsCollapsible = 0
AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = i.ID)
ORDER BY i.SortOrder
", { bizId: bizId }, { datasource: "payfrit" });
categories = [];
for (row in qCategories) {
arrayAppend(categories, { "ItemID": row.ItemID, "ItemName": row.ItemName });
arrayAppend(categories, { "ItemID": row.ID, "Name": row.Name });
}
writeOutput(serializeJSON({

View file

@ -20,10 +20,10 @@ if (len(phone) == 0) {
// Find user by phone
qUser = queryExecute("
SELECT UserID, UserFirstName, UserLastName, UserEmailAddress, UserContactNumber
SELECT ID, FirstName, LastName, EmailAddress, ContactNumber
FROM Users
WHERE REPLACE(REPLACE(REPLACE(UserContactNumber, '-', ''), '(', ''), ')', '') LIKE ?
OR UserContactNumber LIKE ?
WHERE REPLACE(REPLACE(REPLACE(ContactNumber, '-', ''), '(', ''), ')', '') LIKE ?
OR ContactNumber LIKE ?
", [
{ value: "%" & phone & "%", cfsqltype: "cf_sql_varchar" },
{ value: "%" & phone & "%", cfsqltype: "cf_sql_varchar" }
@ -34,36 +34,36 @@ if (qUser.recordCount == 0) {
abort;
}
userId = qUser.UserID;
userId = qUser.ID;
// Get all employee records for this user
qEmployees = queryExecute("
SELECT e.EmployeeID, e.BusinessID, e.EmployeeStatusID,
CAST(e.EmployeeIsActive AS UNSIGNED) AS EmployeeIsActive,
b.BusinessName
FROM lt_Users_Businesses_Employees e
JOIN Businesses b ON e.BusinessID = b.BusinessID
SELECT e.ID, e.BusinessID, e.StatusID,
CAST(e.IsActive AS UNSIGNED) AS IsActive,
b.Name
FROM Employees e
JOIN Businesses b ON e.BusinessID = b.ID
WHERE e.UserID = ?
", [{ value: userId, cfsqltype: "cf_sql_integer" }], { datasource: "payfrit" });
employees = [];
for (row in qEmployees) {
arrayAppend(employees, {
"EmployeeID": row.EmployeeID,
"EmployeeID": row.ID,
"BusinessID": row.BusinessID,
"BusinessName": row.BusinessName,
"StatusID": row.EmployeeStatusID,
"IsActive": row.EmployeeIsActive
"Name": row.Name,
"StatusID": row.StatusID,
"IsActive": row.IsActive
});
}
writeOutput(serializeJSON({
"OK": true,
"USER": {
"UserID": qUser.UserID,
"Name": trim(qUser.UserFirstName & " " & qUser.UserLastName),
"Email": qUser.UserEmailAddress,
"Phone": qUser.UserContactNumber
"UserID": qUser.ID,
"Name": trim(qUser.FirstName & " " & qUser.LastName),
"Email": qUser.EmailAddress,
"Phone": qUser.ContactNumber
},
"EMPLOYEES": employees
}));

View file

@ -28,19 +28,19 @@ try {
response["ERROR"] = "BusinessID required";
} else {
// Get address ID first
qBiz = queryExecute("SELECT BusinessAddressID FROM Businesses WHERE BusinessID = :id", { id: bizID }, { datasource = "payfrit" });
qBiz = queryExecute("SELECT AddressID FROM Businesses WHERE ID = :id", { id: bizID }, { datasource = "payfrit" });
if (qBiz.recordCount == 0) {
response["ERROR"] = "Business not found";
} else {
addrID = qBiz.BusinessAddressID;
addrID = qBiz.AddressID;
// Delete business
queryExecute("DELETE FROM Businesses WHERE BusinessID = :id", { id: bizID }, { datasource = "payfrit" });
queryExecute("DELETE FROM Businesses WHERE ID = :id", { id: bizID }, { datasource = "payfrit" });
// Delete address if exists
if (val(addrID) > 0) {
queryExecute("DELETE FROM Addresses WHERE AddressID = :id", { id: addrID }, { datasource = "payfrit" });
queryExecute("DELETE FROM Addresses WHERE ID = :id", { id: addrID }, { datasource = "payfrit" });
}
response["OK"] = true;

View file

@ -13,7 +13,7 @@
* Delete Orphan Modifiers for In and Out Burger (BusinessID=17)
*
* This script deletes duplicate modifier items that are no longer needed
* because we now use ItemTemplateLinks.
* because we now use lt_ItemID_TemplateItemID.
*
* The orphan items are level-1 modifiers (direct children of parent items)
* that have been replaced by template links.
@ -29,18 +29,18 @@ try {
qOrphans = queryExecute("
SELECT
m.ItemID,
m.ItemName,
m.ItemParentItemID,
p.ItemName as ParentName
m.Name,
m.ParentItemID,
p.Name as ParentName
FROM Items m
INNER JOIN Items p ON p.ItemID = m.ItemParentItemID
INNER JOIN Categories c ON c.CategoryID = p.ItemCategoryID
WHERE c.CategoryBusinessID = :businessID
AND m.ItemParentItemID > 0
AND p.ItemParentItemID = 0
AND (m.ItemIsModifierTemplate IS NULL OR m.ItemIsModifierTemplate = 0)
AND m.ItemIsActive = 1
ORDER BY m.ItemName
INNER JOIN Items p ON p.ItemID = m.ParentItemID
INNER JOIN Categories c ON c.ID = p.CategoryID
WHERE c.BusinessID = :businessID
AND m.ParentItemID > 0
AND p.ParentItemID = 0
AND (m.IsModifierTemplate IS NULL OR m.IsModifierTemplate = 0)
AND m.IsActive = 1
ORDER BY m.Name
", { businessID: businessID }, { datasource: "payfrit" });
arrayAppend(response.deleted, "Found " & qOrphans.recordCount & " orphan modifiers to delete");
@ -50,23 +50,23 @@ try {
try {
// Delete children of this orphan (options within the modifier group)
qDeleteChildren = queryExecute("
DELETE FROM Items WHERE ItemParentItemID = :orphanID
DELETE FROM Items WHERE ParentItemID = :orphanID
", { orphanID: orphan.ItemID }, { datasource: "payfrit" });
// Delete the orphan itself
qDeleteOrphan = queryExecute("
DELETE FROM Items WHERE ItemID = :orphanID
DELETE FROM Items WHERE ID = :orphanID
", { orphanID: orphan.ItemID }, { datasource: "payfrit" });
arrayAppend(response.deleted, {
"ItemID": orphan.ItemID,
"ItemName": orphan.ItemName,
"Name": orphan.Name,
"WasUnder": orphan.ParentName
});
} catch (any deleteErr) {
arrayAppend(response.errors, {
"ItemID": orphan.ItemID,
"ItemName": orphan.ItemName,
"Name": orphan.Name,
"Error": deleteErr.message
});
}

View file

@ -11,7 +11,7 @@
<cfscript>
/**
* Delete orphan Items at ParentID=0
* Orphan = ParentID=0, no children, not in ItemTemplateLinks
* Orphan = ParentID=0, no children, not in lt_ItemID_TemplateItemID
*/
response = { "OK": false, "deleted": 0, "orphans": [] };
@ -19,24 +19,24 @@ response = { "OK": false, "deleted": 0, "orphans": [] };
try {
// Find orphans
qOrphans = queryExecute("
SELECT i.ItemID, i.ItemName, i.ItemBusinessID
SELECT i.ID, i.Name, i.BusinessID
FROM Items i
WHERE i.ItemParentItemID = 0
WHERE i.ParentItemID = 0
AND NOT EXISTS (
SELECT 1 FROM Items child WHERE child.ItemParentItemID = i.ItemID
SELECT 1 FROM Items child WHERE child.ParentItemID = i.ID
)
AND NOT EXISTS (
SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = i.ItemID
SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = i.ID
)
ORDER BY i.ItemBusinessID, i.ItemName
ORDER BY i.BusinessID, i.Name
", {}, { datasource: "payfrit" });
orphanIDs = [];
for (orphan in qOrphans) {
arrayAppend(response.orphans, {
"ItemID": orphan.ItemID,
"ItemName": orphan.ItemName,
"BusinessID": orphan.ItemBusinessID
"Name": orphan.Name,
"BusinessID": orphan.BusinessID
});
arrayAppend(orphanIDs, orphan.ItemID);
}
@ -44,7 +44,7 @@ try {
// Delete them by ID list
if (arrayLen(orphanIDs) > 0) {
queryExecute("
DELETE FROM Items WHERE ItemID IN (#arrayToList(orphanIDs)#)
DELETE FROM Items WHERE ID IN (#arrayToList(orphanIDs)#)
", {}, { datasource: "payfrit" });
}

View file

@ -14,16 +14,16 @@
* Eliminate Categories Table - Schema Migration
*
* Final unified schema:
* - Everything is an Item with ItemBusinessID
* - Everything is an Item with BusinessID
* - ParentID=0 items are either categories or templates (derived from usage)
* - Categories = items at ParentID=0 that have menu items as children
* - Templates = items at ParentID=0 that appear in ItemTemplateLinks
* - Templates = items at ParentID=0 that appear in lt_ItemID_TemplateItemID
* - Orphans = ParentID=0 items that are neither (cleanup candidates)
*
* Steps:
* 1. Add ItemBusinessID column to Items
* 1. Add BusinessID column to Items
* 2. For each Category: create Item, re-parent menu items, set BusinessID
* 3. Set ItemBusinessID on templates based on linked items
* 3. Set BusinessID on templates based on linked items
*
* Query param: ?dryRun=1 to preview without making changes
*/
@ -34,30 +34,30 @@ try {
dryRun = structKeyExists(url, "dryRun") && url.dryRun == 1;
response["dryRun"] = dryRun;
// Step 1: Add ItemBusinessID column if it doesn't exist
// Step 1: Add BusinessID column if it doesn't exist
try {
if (!dryRun) {
queryExecute("
ALTER TABLE Items ADD COLUMN ItemBusinessID INT DEFAULT 0 AFTER ItemID
ALTER TABLE Items ADD COLUMN BusinessID INT DEFAULT 0 AFTER ItemID
", {}, { datasource: "payfrit" });
}
arrayAppend(response.steps, "Added ItemBusinessID column to Items table");
arrayAppend(response.steps, "Added BusinessID column to Items table");
} catch (any e) {
if (findNoCase("Duplicate column", e.message)) {
arrayAppend(response.steps, "ItemBusinessID column already exists");
arrayAppend(response.steps, "BusinessID column already exists");
} else {
throw(e);
}
}
// Step 2: Add index on ItemBusinessID
// Step 2: Add index on BusinessID
try {
if (!dryRun) {
queryExecute("
CREATE INDEX idx_item_business ON Items (ItemBusinessID)
CREATE INDEX idx_item_business ON Items (BusinessID)
", {}, { datasource: "payfrit" });
}
arrayAppend(response.steps, "Added index on ItemBusinessID");
arrayAppend(response.steps, "Added index on BusinessID");
} catch (any e) {
if (findNoCase("Duplicate key name", e.message)) {
arrayAppend(response.steps, "Index idx_item_business already exists");
@ -66,7 +66,7 @@ try {
}
}
// Step 3: Drop foreign key constraint on ItemCategoryID if it exists
// Step 3: Drop foreign key constraint on CategoryID if it exists
try {
if (!dryRun) {
queryExecute("
@ -84,9 +84,9 @@ try {
// Step 4: Get all Categories
qCategories = queryExecute("
SELECT CategoryID, CategoryBusinessID, CategoryName
SELECT ID, BusinessID, Name
FROM Categories
ORDER BY CategoryBusinessID, CategoryName
ORDER BY BusinessID, Name
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Found " & qCategories.recordCount & " categories to migrate");
@ -94,29 +94,29 @@ try {
// Step 4: Migrate each category
for (cat in qCategories) {
migration = {
"oldCategoryID": cat.CategoryID,
"categoryName": cat.CategoryName,
"businessID": cat.CategoryBusinessID,
"oldCategoryID": cat.ID,
"categoryName": cat.Name,
"businessID": cat.BusinessID,
"newItemID": 0,
"itemsUpdated": 0
};
if (!dryRun) {
// Create new Item for this category (ParentID=0, no template flag needed)
// Note: ItemCategoryID set to 0 temporarily until we drop that column
// Note: CategoryID set to 0 temporarily until we drop that column
queryExecute("
INSERT INTO Items (
ItemBusinessID,
ItemCategoryID,
ItemName,
ItemDescription,
ItemParentItemID,
ItemPrice,
ItemIsActive,
ItemIsCheckedByDefault,
ItemRequiresChildSelection,
ItemSortOrder,
ItemAddedOn
BusinessID,
CategoryID,
Name,
Description,
ParentItemID,
Price,
IsActive,
IsCheckedByDefault,
RequiresChildSelection,
SortOrder,
AddedOn
) VALUES (
:businessID,
0,
@ -131,56 +131,56 @@ try {
NOW()
)
", {
businessID: cat.CategoryBusinessID,
categoryName: cat.CategoryName
businessID: cat.BusinessID,
categoryName: cat.Name
}, { datasource: "payfrit" });
// Get the new Item ID
qNewItem = queryExecute("
SELECT ItemID FROM Items
WHERE ItemBusinessID = :businessID
AND ItemName = :categoryName
AND ItemParentItemID = 0
ORDER BY ItemID DESC
SELECT ID FROM Items
WHERE BusinessID = :businessID
AND Name = :categoryName
AND ParentItemID = 0
ORDER BY ID DESC
LIMIT 1
", {
businessID: cat.CategoryBusinessID,
categoryName: cat.CategoryName
businessID: cat.BusinessID,
categoryName: cat.Name
}, { datasource: "payfrit" });
newItemID = qNewItem.ItemID;
newItemID = qNewItem.ID;
migration["newItemID"] = newItemID;
// Update menu items in this category:
// - Set ItemParentItemID = newItemID (for top-level items only)
// - Set ItemBusinessID = businessID (for all items)
// - Set ParentItemID = newItemID (for top-level items only)
// - Set BusinessID = businessID (for all items)
queryExecute("
UPDATE Items
SET ItemBusinessID = :businessID,
ItemParentItemID = :newItemID
WHERE ItemCategoryID = :categoryID
AND ItemParentItemID = 0
SET BusinessID = :businessID,
ParentItemID = :newItemID
WHERE CategoryID = :categoryID
AND ParentItemID = 0
", {
businessID: cat.CategoryBusinessID,
businessID: cat.BusinessID,
newItemID: newItemID,
categoryID: cat.CategoryID
categoryID: cat.ID
}, { datasource: "payfrit" });
// Set ItemBusinessID on ALL items in this category (including nested)
// Set BusinessID on ALL items in this category (including nested)
queryExecute("
UPDATE Items
SET ItemBusinessID = :businessID
WHERE ItemCategoryID = :categoryID
AND (ItemBusinessID IS NULL OR ItemBusinessID = 0)
SET BusinessID = :businessID
WHERE CategoryID = :categoryID
AND (BusinessID IS NULL OR BusinessID = 0)
", {
businessID: cat.CategoryBusinessID,
categoryID: cat.CategoryID
businessID: cat.BusinessID,
categoryID: cat.ID
}, { datasource: "payfrit" });
// Count how many were updated
qCount = queryExecute("
SELECT COUNT(*) as cnt FROM Items
WHERE ItemParentItemID = :newItemID
WHERE ParentItemID = :newItemID
", { newItemID: newItemID }, { datasource: "payfrit" });
migration["itemsUpdated"] = qCount.cnt;
@ -188,9 +188,9 @@ try {
// Dry run - count what would be updated
qCount = queryExecute("
SELECT COUNT(*) as cnt FROM Items
WHERE ItemCategoryID = :categoryID
AND ItemParentItemID = 0
", { categoryID: cat.CategoryID }, { datasource: "payfrit" });
WHERE CategoryID = :categoryID
AND ParentItemID = 0
", { categoryID: cat.ID }, { datasource: "payfrit" });
migration["itemsToUpdate"] = qCount.cnt;
}
@ -198,37 +198,37 @@ try {
arrayAppend(response.migrations, migration);
}
// Step 5: Set ItemBusinessID for templates (items in ItemTemplateLinks)
// Step 5: Set BusinessID for templates (items in lt_ItemID_TemplateItemID)
// Templates get their BusinessID from the items they're linked to
if (!dryRun) {
queryExecute("
UPDATE Items t
INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = t.ItemID
INNER JOIN Items i ON i.ItemID = tl.ItemID
SET t.ItemBusinessID = i.ItemBusinessID
WHERE (t.ItemBusinessID IS NULL OR t.ItemBusinessID = 0)
AND i.ItemBusinessID > 0
INNER JOIN lt_ItemID_TemplateItemID tl ON tl.TemplateItemID = t.ItemID
INNER JOIN Items i ON i.ID = tl.ItemID
SET t.BusinessID = i.BusinessID
WHERE (t.BusinessID IS NULL OR t.BusinessID = 0)
AND i.BusinessID > 0
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Set ItemBusinessID on templates from linked items");
arrayAppend(response.steps, "Set BusinessID on templates from linked items");
// Set ItemBusinessID on template children (options)
// Set BusinessID on template children (options)
queryExecute("
UPDATE Items c
INNER JOIN Items t ON t.ItemID = c.ItemParentItemID
SET c.ItemBusinessID = t.ItemBusinessID
WHERE t.ItemBusinessID > 0
AND (c.ItemBusinessID IS NULL OR c.ItemBusinessID = 0)
INNER JOIN Items t ON t.ItemID = c.ParentItemID
SET c.BusinessID = t.BusinessID
WHERE t.BusinessID > 0
AND (c.BusinessID IS NULL OR c.BusinessID = 0)
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Set ItemBusinessID on template children");
arrayAppend(response.steps, "Set BusinessID on template children");
// Make sure templates have ParentID=0 (they live at top level)
queryExecute("
UPDATE Items t
INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = t.ItemID
SET t.ItemParentItemID = 0
WHERE t.ItemParentItemID != 0
INNER JOIN lt_ItemID_TemplateItemID tl ON tl.TemplateItemID = t.ItemID
SET t.ParentItemID = 0
WHERE t.ParentItemID != 0
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Ensured templates have ParentItemID=0");

View file

@ -14,32 +14,32 @@ fakeCategories = [11177, 11180, 11183, 11186, 11190, 11193, 11196, 11199, 11204,
// Deactivate these items (or we could delete them, but deactivate is safer)
for (itemId in fakeCategories) {
queryExecute("
UPDATE Items SET ItemIsActive = 0 WHERE ItemID = :itemId AND ItemBusinessID = :bizId
UPDATE Items SET IsActive = 0 WHERE ItemID = :itemId AND BusinessID = :bizId
", { itemId: itemId, bizId: bizId }, { datasource: "payfrit" });
}
// Also deactivate their children (modifier options that belong to these fake parents)
for (itemId in fakeCategories) {
queryExecute("
UPDATE Items SET ItemIsActive = 0 WHERE ItemParentItemID = :itemId AND ItemBusinessID = :bizId
UPDATE Items SET IsActive = 0 WHERE ParentItemID = :itemId AND BusinessID = :bizId
", { itemId: itemId, bizId: bizId }, { datasource: "payfrit" });
}
// Now verify what categories remain
qCategories = queryExecute("
SELECT i.ItemID, i.ItemName
SELECT i.ID, i.Name
FROM Items i
WHERE i.ItemBusinessID = :bizId
AND i.ItemParentItemID = 0
AND i.ItemIsActive = 1
AND i.ItemIsCollapsible = 0
AND NOT EXISTS (SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = i.ItemID)
ORDER BY i.ItemSortOrder
WHERE i.BusinessID = :bizId
AND i.ParentItemID = 0
AND i.IsActive = 1
AND i.IsCollapsible = 0
AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = i.ID)
ORDER BY i.SortOrder
", { bizId: bizId }, { datasource: "payfrit" });
categories = [];
for (row in qCategories) {
arrayAppend(categories, { "ItemID": row.ItemID, "ItemName": row.ItemName });
arrayAppend(categories, { "ItemID": row.ID, "Name": row.Name });
}
writeOutput(serializeJSON({

View file

@ -21,8 +21,8 @@ actions = [];
// First, let's see what templates already exist and are active for burgers
qExistingLinks = queryExecute("
SELECT tl.ItemID as MenuItemID, tl.TemplateItemID, t.ItemName as TemplateName
FROM ItemTemplateLinks tl
SELECT tl.ItemID as MenuItemID, tl.TemplateItemID, t.Name as TemplateName
FROM lt_ItemID_TemplateItemID tl
JOIN Items t ON t.ItemID = tl.TemplateItemID
WHERE tl.ItemID IN (:burgerIds)
", { burgerIds: { value: arrayToList(burgerIds), list: true } }, { datasource: "payfrit" });
@ -33,8 +33,8 @@ for (row in qExistingLinks) {
// Reactivate template 11196 (Extras with Add Cheese)
if (!dryRun) {
queryExecute("UPDATE Items SET ItemIsActive = 1 WHERE ItemID = 11196", {}, { datasource: "payfrit" });
queryExecute("UPDATE Items SET ItemIsActive = 1 WHERE ItemParentItemID = 11196", {}, { datasource: "payfrit" });
queryExecute("UPDATE Items SET IsActive = 1 WHERE ItemID = 11196", {}, { datasource: "payfrit" });
queryExecute("UPDATE Items SET IsActive = 1 WHERE ParentItemID = 11196", {}, { datasource: "payfrit" });
}
arrayAppend(actions, { action: dryRun ? "WOULD_REACTIVATE" : "REACTIVATED", itemID: 11196, name: "Extras (Add Cheese, Add Onions)" });
@ -42,13 +42,13 @@ arrayAppend(actions, { action: dryRun ? "WOULD_REACTIVATE" : "REACTIVATED", item
for (burgerId in burgerIds) {
// Check if link already exists
qCheck = queryExecute("
SELECT COUNT(*) as cnt FROM ItemTemplateLinks WHERE ItemID = :burgerId AND TemplateItemID = 11196
SELECT COUNT(*) as cnt FROM lt_ItemID_TemplateItemID WHERE ItemID = :burgerId AND TemplateItemID = 11196
", { burgerId: burgerId }, { datasource: "payfrit" });
if (qCheck.cnt EQ 0) {
if (!dryRun) {
queryExecute("
INSERT INTO ItemTemplateLinks (ItemID, TemplateItemID, SortOrder)
INSERT INTO lt_ItemID_TemplateItemID (ItemID, TemplateItemID, SortOrder)
VALUES (:burgerId, 11196, 2)
", { burgerId: burgerId }, { datasource: "payfrit" });
}
@ -59,17 +59,17 @@ for (burgerId in burgerIds) {
// Verify the result
if (!dryRun) {
qVerify = queryExecute("
SELECT mi.ItemID, mi.ItemName, GROUP_CONCAT(t.ItemName ORDER BY tl.SortOrder) as Templates
SELECT mi.ID, mi.Name, GROUP_CONCAT(t.Name ORDER BY tl.SortOrder) as Templates
FROM Items mi
LEFT JOIN ItemTemplateLinks tl ON tl.ItemID = mi.ItemID
LEFT JOIN lt_ItemID_TemplateItemID tl ON tl.ItemID = mi.ID
LEFT JOIN Items t ON t.ItemID = tl.TemplateItemID
WHERE mi.ItemID IN (:burgerIds)
GROUP BY mi.ItemID, mi.ItemName
WHERE mi.ID IN (:burgerIds)
GROUP BY mi.ID, mi.Name
", { burgerIds: { value: arrayToList(burgerIds), list: true } }, { datasource: "payfrit" });
result = [];
for (row in qVerify) {
arrayAppend(result, { itemID: row.ItemID, name: row.ItemName, templates: row.Templates });
arrayAppend(result, { itemID: row.ID, name: row.Name, templates: row.Templates });
}
} else {
result = "Dry run - no changes made";

View file

@ -0,0 +1,38 @@
<cfsetting showdebugoutput="false">
<cfcontent type="application/json" reset="true">
<cfscript>
// One-time fix: remove # prefix from BrandColor
qBefore = queryExecute("
SELECT ID, BrandColor
FROM Businesses
WHERE BrandColor LIKE :pattern
", { pattern: { value: "##%", cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
if (qBefore.recordCount > 0) {
queryExecute("
UPDATE Businesses
SET BrandColor = SUBSTRING(BrandColor, 2)
WHERE BrandColor LIKE :pattern
", { pattern: { value: "##%", cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
}
qAfter = queryExecute("
SELECT ID, BrandColor
FROM Businesses
WHERE BrandColor IS NOT NULL AND LENGTH(BrandColor) > 0
", {}, { datasource: "payfrit" });
rows = [];
for (i = 1; i <= qAfter.recordCount; i++) {
arrayAppend(rows, {
"BusinessID": qAfter.BusinessID[i],
"BrandColor": qAfter.BrandColor[i]
});
}
writeOutput(serializeJSON({
"OK": true,
"FIXED": qBefore.recordCount,
"CURRENT": rows
}));
</cfscript>

View file

@ -0,0 +1,37 @@
<cfsetting showdebugoutput="false">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
// Fix orphaned tasks that had old category ID 4 (deleted Service Point)
// Update them to use new category ID 25 (new Service Point)
try {
// First check how many tasks are affected
qCount = queryExecute("
SELECT COUNT(*) as cnt FROM Tasks WHERE TaskCategoryID = 4
", [], { datasource: "payfrit" });
affectedCount = qCount.cnt;
if (affectedCount > 0) {
// Update them to the new category
queryExecute("
UPDATE Tasks SET TaskCategoryID = 25 WHERE TaskCategoryID = 4
", [], { datasource: "payfrit" });
writeOutput(serializeJSON({
"OK": true,
"MESSAGE": "Updated " & affectedCount & " tasks from category 4 to category 25"
}));
} else {
writeOutput(serializeJSON({
"OK": true,
"MESSAGE": "No tasks found with category ID 4"
}));
}
} catch (any e) {
writeOutput(serializeJSON({
"OK": false,
"ERROR": e.message
}));
}
</cfscript>

View file

@ -30,8 +30,8 @@
* 2. Re-parent the three flavors under the new group
* 3. Remove template flag from flavors
* 4. Mark "Choose Flavor" as the template
* 5. Create ItemTemplateLinks entry for Shake -> Choose Flavor
* 6. Set ItemRequiresChildSelection=1 on the shake item
* 5. Create lt_ItemID_TemplateItemID entry for Shake -> Choose Flavor
* 6. Set RequiresChildSelection=1 on the shake item
*/
response = { "OK": false, "steps": [] };
@ -46,19 +46,19 @@ try {
// Step 1: Create "Choose Flavor" modifier group under Shake
queryExecute("
INSERT INTO Items (
ItemCategoryID,
ItemName,
ItemDescription,
ItemParentItemID,
ItemPrice,
ItemIsActive,
ItemIsCheckedByDefault,
ItemRequiresChildSelection,
ItemMaxNumSelectionReq,
ItemIsCollapsible,
ItemSortOrder,
ItemIsModifierTemplate,
ItemAddedOn
CategoryID,
Name,
Description,
ParentItemID,
Price,
IsActive,
IsCheckedByDefault,
RequiresChildSelection,
MaxNumSelectionReq,
IsCollapsible,
SortOrder,
IsModifierTemplate,
AddedOn
) VALUES (
:categoryID,
'Choose Flavor',
@ -81,22 +81,22 @@ try {
// Get the new Choose Flavor ID
qNewGroup = queryExecute("
SELECT ItemID FROM Items
WHERE ItemName = 'Choose Flavor'
AND ItemParentItemID = :shakeItemID
ORDER BY ItemID DESC
SELECT ID FROM Items
WHERE Name = 'Choose Flavor'
AND ParentItemID = :shakeItemID
ORDER BY ID DESC
LIMIT 1
", { shakeItemID: shakeItemID }, { datasource: "payfrit" });
chooseFlavorID = qNewGroup.ItemID;
chooseFlavorID = qNewGroup.ID;
arrayAppend(response.steps, "Created 'Choose Flavor' group with ID: " & chooseFlavorID);
// Step 2: Re-parent the three flavors under Choose Flavor
queryExecute("
UPDATE Items
SET ItemParentItemID = :chooseFlavorID,
ItemIsModifierTemplate = 0,
ItemIsCheckedByDefault = 0
SET ParentItemID = :chooseFlavorID,
IsModifierTemplate = 0,
IsCheckedByDefault = 0
WHERE ItemID IN (:chocolateID, :strawberryID, :vanillaID)
", {
chooseFlavorID: chooseFlavorID,
@ -109,14 +109,14 @@ try {
// Step 3: Set Vanilla as default (common choice)
queryExecute("
UPDATE Items SET ItemIsCheckedByDefault = 1 WHERE ItemID = :vanillaID
UPDATE Items SET IsCheckedByDefault = 1 WHERE ItemID = :vanillaID
", { vanillaID: vanillaID }, { datasource: "payfrit" });
arrayAppend(response.steps, "Set Vanilla as default flavor");
// Step 4: Remove old template links for the flavors
queryExecute("
DELETE FROM ItemTemplateLinks
DELETE FROM lt_ItemID_TemplateItemID
WHERE TemplateItemID IN (:chocolateID, :strawberryID, :vanillaID)
", {
chocolateID: chocolateID,
@ -126,9 +126,9 @@ try {
arrayAppend(response.steps, "Removed old template links for flavor items");
// Step 5: Create ItemTemplateLinks for Shake -> Choose Flavor
// Step 5: Create lt_ItemID_TemplateItemID for Shake -> Choose Flavor
queryExecute("
INSERT INTO ItemTemplateLinks (ItemID, TemplateItemID, SortOrder)
INSERT INTO lt_ItemID_TemplateItemID (ItemID, TemplateItemID, SortOrder)
VALUES (:shakeItemID, :chooseFlavorID, 0)
ON DUPLICATE KEY UPDATE SortOrder = 0
", {
@ -138,14 +138,14 @@ try {
arrayAppend(response.steps, "Created template link: Shake -> Choose Flavor");
// Step 6: Set ItemRequiresChildSelection on shake
// Step 6: Set RequiresChildSelection on shake
queryExecute("
UPDATE Items
SET ItemRequiresChildSelection = 1
SET RequiresChildSelection = 1
WHERE ItemID = :shakeItemID
", { shakeItemID: shakeItemID }, { datasource: "payfrit" });
arrayAppend(response.steps, "Set ItemRequiresChildSelection=1 on Shake item");
arrayAppend(response.steps, "Set RequiresChildSelection=1 on Shake item");
response["OK"] = true;
response["chooseFlavorID"] = chooseFlavorID;

View file

@ -75,17 +75,17 @@ function buildAddressString(line1, line2, city, zipCode) {
<cfif structKeyExists(url, "geocode") AND structKeyExists(url, "addressId")>
<cfset addressId = val(url.addressId)>
<cfquery name="addr" datasource="payfrit">
SELECT AddressLine1, AddressLine2, AddressCity, AddressZIPCode
FROM Addresses WHERE AddressID = <cfqueryparam value="#addressId#" cfsqltype="cf_sql_integer">
SELECT Line1, Line2, City, ZIPCode
FROM Addresses WHERE ID = <cfqueryparam value="#addressId#" cfsqltype="cf_sql_integer">
</cfquery>
<cfif addr.recordCount GT 0>
<cfset fullAddress = buildAddressString(addr.AddressLine1, addr.AddressLine2, addr.AddressCity, addr.AddressZIPCode)>
<cfset fullAddress = buildAddressString(addr.Line1, addr.Line2, addr.City, addr.ZIPCode)>
<cfset geo = geocodeAddress(fullAddress)>
<cfif geo.success>
<cfquery datasource="payfrit">
UPDATE Addresses SET AddressLat = <cfqueryparam value="#geo.lat#" cfsqltype="cf_sql_decimal">,
AddressLng = <cfqueryparam value="#geo.lng#" cfsqltype="cf_sql_decimal">
WHERE AddressID = <cfqueryparam value="#addressId#" cfsqltype="cf_sql_integer">
UPDATE Addresses SET Latitude = <cfqueryparam value="#geo.lat#" cfsqltype="cf_sql_decimal">,
Longitude = <cfqueryparam value="#geo.lng#" cfsqltype="cf_sql_decimal">
WHERE ID = <cfqueryparam value="#addressId#" cfsqltype="cf_sql_integer">
</cfquery>
<cfoutput><div class="success">Geocoded Address ID #addressId#: #geo.lat#, #geo.lng#</div></cfoutput>
<cfelse>
@ -96,21 +96,21 @@ function buildAddressString(line1, line2, city, zipCode) {
<cfif structKeyExists(url, "geocodeAll")>
<cfquery name="missing" datasource="payfrit">
SELECT AddressID, AddressLine1, AddressLine2, AddressCity, AddressZIPCode
SELECT ID, Line1, Line2, City, ZIPCode
FROM Addresses
WHERE (AddressLat IS NULL OR AddressLat = 0)
AND AddressLine1 IS NOT NULL AND AddressLine1 != ''
WHERE (Latitude IS NULL OR Latitude = 0)
AND Line1 IS NOT NULL AND Line1 != ''
</cfquery>
<cfset successCount = 0>
<cfset failCount = 0>
<cfloop query="missing">
<cfset fullAddress = buildAddressString(missing.AddressLine1, missing.AddressLine2, missing.AddressCity, missing.AddressZIPCode)>
<cfset fullAddress = buildAddressString(missing.Line1, missing.Line2, missing.City, missing.ZIPCode)>
<cfset geo = geocodeAddress(fullAddress)>
<cfif geo.success>
<cfquery datasource="payfrit">
UPDATE Addresses SET AddressLat = <cfqueryparam value="#geo.lat#" cfsqltype="cf_sql_decimal">,
AddressLng = <cfqueryparam value="#geo.lng#" cfsqltype="cf_sql_decimal">
WHERE AddressID = <cfqueryparam value="#missing.AddressID#" cfsqltype="cf_sql_integer">
UPDATE Addresses SET Latitude = <cfqueryparam value="#geo.lat#" cfsqltype="cf_sql_decimal">,
Longitude = <cfqueryparam value="#geo.lng#" cfsqltype="cf_sql_decimal">
WHERE ID = <cfqueryparam value="#missing.ID#" cfsqltype="cf_sql_integer">
</cfquery>
<cfset successCount = successCount + 1>
<cfelse>
@ -123,23 +123,23 @@ function buildAddressString(line1, line2, city, zipCode) {
<cfquery name="addresses" datasource="payfrit">
SELECT
b.BusinessID,
b.BusinessName,
a.AddressID,
a.AddressLine1,
a.AddressLine2,
a.AddressCity,
a.AddressZIPCode,
a.AddressLat,
a.AddressLng
b.ID,
b.Name,
a.ID,
a.Line1,
a.Line2,
a.City,
a.ZIPCode,
a.Latitude,
a.Longitude
FROM Businesses b
LEFT JOIN Addresses a ON b.BusinessAddressID = a.AddressID
ORDER BY b.BusinessName
LEFT JOIN Addresses a ON b.AddressID = a.ID
ORDER BY b.Name
</cfquery>
<cfset missingCount = 0>
<cfloop query="addresses">
<cfif (NOT len(addresses.AddressLat) OR val(addresses.AddressLat) EQ 0) AND len(addresses.AddressLine1)>
<cfif (NOT len(addresses.Latitude) OR val(addresses.Latitude) EQ 0) AND len(addresses.Line1)>
<cfset missingCount = missingCount + 1>
</cfif>
</cfloop>
@ -166,31 +166,31 @@ function buildAddressString(line1, line2, city, zipCode) {
<cfloop query="addresses">
<tr>
<td>
#addresses.BusinessName#
<cfif len(addresses.AddressLat) AND val(addresses.AddressLat) NEQ 0>
#addresses.Name#
<cfif len(addresses.Latitude) AND val(addresses.Latitude) NEQ 0>
<span class="has-coords">#chr(10003)#</span>
<cfelseif len(addresses.AddressLine1)>
<cfelseif len(addresses.Line1)>
<span class="no-coords">#chr(9679)#</span>
</cfif>
</td>
<td class="address">
<cfif len(addresses.AddressLine1)>
#addresses.AddressLine1#<cfif len(addresses.AddressLine2)>, #addresses.AddressLine2#</cfif><br>
#addresses.AddressCity# #addresses.AddressZIPCode#
<cfif len(addresses.Line1)>
#addresses.Line1#<cfif len(addresses.Line2)>, #addresses.Line2#</cfif><br>
#addresses.City# #addresses.ZIPCode#
<cfelse>
<em style="color:##666;">No address</em>
</cfif>
</td>
<td class="coords">
<cfif len(addresses.AddressLat) AND val(addresses.AddressLat) NEQ 0>
#numberFormat(addresses.AddressLat, "_.______")#<br>
#numberFormat(addresses.AddressLng, "_.______")#
<cfif len(addresses.Latitude) AND val(addresses.Latitude) NEQ 0>
#numberFormat(addresses.Latitude, "_.______")#<br>
#numberFormat(addresses.Longitude, "_.______")#
<cfelse>
-
</cfif>
</td>
<td>
<cfif len(addresses.AddressLine1) AND len(addresses.AddressID)>
<cfif len(addresses.Line1) AND len(addresses.AddressID)>
<a href="?geocode=1&addressId=#addresses.AddressID#"><button class="btn-lookup">Lookup</button></a>
</cfif>
</td>

View file

@ -29,7 +29,7 @@ try {
response["ERROR"] = "ChildBusinessID required";
} else {
queryExecute("
UPDATE Businesses SET BusinessParentBusinessID = :parentId WHERE BusinessID = :childId
UPDATE Businesses SET ParentBusinessID = :parentId WHERE ID = :childId
", {
parentId: { value = parentID > 0 ? parentID : javaCast("null", ""), cfsqltype = "cf_sql_integer", null = parentID == 0 },
childId: childID

View file

@ -26,13 +26,13 @@ try {
// Step 1: Get all parent items (burgers, combos, etc.)
qParentItems = queryExecute("
SELECT i.ItemID, i.ItemName
SELECT i.ID, i.Name
FROM Items i
INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID
WHERE c.CategoryBusinessID = :businessID
AND i.ItemParentItemID = 0
AND i.ItemIsActive = 1
ORDER BY i.ItemName
INNER JOIN Categories c ON c.ID = i.CategoryID
WHERE c.BusinessID = :businessID
AND i.ParentItemID = 0
AND i.IsActive = 1
ORDER BY i.Name
", { businessID: businessID }, { datasource: "payfrit" });
arrayAppend(response.steps, "Found " & qParentItems.recordCount & " parent items");
@ -41,20 +41,20 @@ try {
qModifiers = queryExecute("
SELECT
m.ItemID,
m.ItemName,
m.ItemParentItemID,
m.ItemPrice,
m.ItemIsCheckedByDefault,
m.ItemSortOrder,
p.ItemName as ParentName
m.Name,
m.ParentItemID,
m.Price,
m.IsCheckedByDefault,
m.SortOrder,
p.Name as ParentName
FROM Items m
INNER JOIN Items p ON p.ItemID = m.ItemParentItemID
INNER JOIN Categories c ON c.CategoryID = p.ItemCategoryID
WHERE c.CategoryBusinessID = :businessID
AND m.ItemParentItemID > 0
AND m.ItemIsActive = 1
AND p.ItemParentItemID = 0
ORDER BY m.ItemName, m.ItemID
INNER JOIN Items p ON p.ItemID = m.ParentItemID
INNER JOIN Categories c ON c.ID = p.CategoryID
WHERE c.BusinessID = :businessID
AND m.ParentItemID > 0
AND m.IsActive = 1
AND p.ParentItemID = 0
ORDER BY m.Name, m.ItemID
", { businessID: businessID }, { datasource: "payfrit" });
arrayAppend(response.steps, "Found " & qModifiers.recordCount & " level-1 modifiers");
@ -62,17 +62,17 @@ try {
// Step 3: Group modifiers by name to find duplicates
modifiersByName = {};
for (mod in qModifiers) {
modName = mod.ItemName;
modName = mod.Name;
if (!structKeyExists(modifiersByName, modName)) {
modifiersByName[modName] = [];
}
arrayAppend(modifiersByName[modName], {
"ItemID": mod.ItemID,
"ParentItemID": mod.ItemParentItemID,
"ParentItemID": mod.ParentItemID,
"ParentName": mod.ParentName,
"Price": mod.ItemPrice,
"IsDefault": mod.ItemIsCheckedByDefault,
"SortOrder": mod.ItemSortOrder
"Price": mod.Price,
"IsDefault": mod.IsCheckedByDefault,
"SortOrder": mod.SortOrder
});
}
@ -91,7 +91,7 @@ try {
// Mark as template
queryExecute("
UPDATE Items SET ItemIsModifierTemplate = 1 WHERE ItemID = :itemID
UPDATE Items SET IsModifierTemplate = 1 WHERE ItemID = :itemID
", { itemID: templateItemID }, { datasource: "payfrit" });
templateMap[modName] = templateItemID;
@ -111,7 +111,7 @@ try {
// Insert link (ignore duplicates)
try {
queryExecute("
INSERT INTO ItemTemplateLinks (ItemID, TemplateItemID, SortOrder)
INSERT INTO lt_ItemID_TemplateItemID (ItemID, TemplateItemID, SortOrder)
VALUES (:itemID, :templateID, :sortOrder)
ON DUPLICATE KEY UPDATE SortOrder = :sortOrder
", {
@ -134,7 +134,7 @@ try {
if (i > 1) {
arrayAppend(orphanItems, {
"ItemID": inst.ItemID,
"ItemName": modName,
"Name": modName,
"WasUnder": inst.ParentName
});
}
@ -143,12 +143,12 @@ try {
// Single instance - still mark as template for consistency
singleItem = instances[1];
queryExecute("
UPDATE Items SET ItemIsModifierTemplate = 1 WHERE ItemID = :itemID
UPDATE Items SET IsModifierTemplate = 1 WHERE ItemID = :itemID
", { itemID: singleItem.ItemID }, { datasource: "payfrit" });
// Create link
queryExecute("
INSERT INTO ItemTemplateLinks (ItemID, TemplateItemID, SortOrder)
INSERT INTO lt_ItemID_TemplateItemID (ItemID, TemplateItemID, SortOrder)
VALUES (:itemID, :templateID, :sortOrder)
ON DUPLICATE KEY UPDATE SortOrder = :sortOrder
", {

View file

@ -27,20 +27,20 @@ try {
// Find all businesses with items in unified schema
if (len(businessFilter)) {
qBusinesses = queryExecute("
SELECT DISTINCT ItemBusinessID as BusinessID
SELECT DISTINCT BusinessID as BusinessID
FROM Items
WHERE ItemBusinessID = :bid AND ItemBusinessID > 0
WHERE BusinessID = :bid AND BusinessID > 0
", { bid: businessFilter }, { datasource: "payfrit" });
} else {
qBusinesses = queryExecute("
SELECT DISTINCT ItemBusinessID as BusinessID
SELECT DISTINCT BusinessID as BusinessID
FROM Items
WHERE ItemBusinessID > 0
WHERE BusinessID > 0
", {}, { datasource: "payfrit" });
}
for (biz in qBusinesses) {
bizId = biz.BusinessID;
bizId = biz.ID;
bizResult = { "BusinessID": bizId, "CategoriesCreated": 0, "ItemsUpdated": 0 };
try {
@ -48,26 +48,26 @@ try {
qCategoryItems = queryExecute("
SELECT DISTINCT
p.ItemID,
p.ItemName,
p.ItemSortOrder
p.Name,
p.SortOrder
FROM Items p
INNER JOIN Items c ON c.ItemParentItemID = p.ItemID
WHERE p.ItemBusinessID = :bizId
AND p.ItemParentItemID = 0
AND (p.ItemIsCollapsible = 0 OR p.ItemIsCollapsible IS NULL)
INNER JOIN Items c ON c.ParentItemID = p.ItemID
WHERE p.BusinessID = :bizId
AND p.ParentItemID = 0
AND (p.IsCollapsible = 0 OR p.IsCollapsible IS NULL)
AND NOT EXISTS (
SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = p.ItemID
SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = p.ItemID
)
ORDER BY p.ItemSortOrder, p.ItemName
ORDER BY p.SortOrder, p.Name
", { bizId: bizId }, { datasource: "payfrit" });
sortOrder = 0;
for (catItem in qCategoryItems) {
// Check if category already exists for this business with same name
qExisting = queryExecute("
SELECT CategoryID FROM Categories
WHERE CategoryBusinessID = :bizId AND CategoryName = :name
", { bizId: bizId, name: left(catItem.ItemName, 30) }, { datasource: "payfrit" });
SELECT ID FROM Categories
WHERE BusinessID = :bizId AND Name = :name
", { bizId: bizId, name: left(catItem.Name, 30) }, { datasource: "payfrit" });
if (qExisting.recordCount == 0) {
// Get next CategoryID
@ -79,12 +79,12 @@ try {
// Create new category with explicit ID
queryExecute("
INSERT INTO Categories
(CategoryID, CategoryBusinessID, CategoryParentCategoryID, CategoryName, CategorySortOrder, CategoryAddedOn)
(CategoryID, BusinessID, ParentCategoryID, Name, SortOrder, AddedOn)
VALUES (:catId, :bizId, 0, :name, :sortOrder, NOW())
", {
catId: newCatId,
bizId: bizId,
name: left(catItem.ItemName, 30),
name: left(catItem.Name, 30),
sortOrder: sortOrder
}, { datasource: "payfrit" });
@ -96,9 +96,9 @@ try {
// Update all children of this category Item to have the new CategoryID
queryExecute("
UPDATE Items
SET ItemCategoryID = :catId
WHERE ItemParentItemID = :parentId
AND ItemBusinessID = :bizId
SET CategoryID = :catId
WHERE ParentItemID = :parentId
AND BusinessID = :bizId
", {
catId: newCatId,
parentId: catItem.ItemID,

View file

@ -0,0 +1,299 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="false">
<cfscript>
// Localhost-only protection
remoteAddr = cgi.REMOTE_ADDR;
if (remoteAddr != "127.0.0.1" && remoteAddr != "::1" && remoteAddr != "0:0:0:0:0:0:0:1" && remoteAddr != "10.10.0.2") {
writeOutput("Forbidden"); abort;
}
</cfscript>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>API Performance Dashboard</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: #0f1117; color: #e1e4e8; padding: 20px; }
h1 { font-size: 22px; font-weight: 600; margin-bottom: 16px; color: #fff; }
.controls { display: flex; gap: 10px; margin-bottom: 20px; align-items: center; flex-wrap: wrap; }
.controls label { font-size: 13px; color: #8b949e; }
.controls select, .controls input { background: #1c1f26; border: 1px solid #30363d; color: #e1e4e8; padding: 6px 10px; border-radius: 6px; font-size: 13px; }
.controls button { background: #238636; color: #fff; border: none; padding: 6px 16px; border-radius: 6px; font-size: 13px; cursor: pointer; font-weight: 500; }
.controls button:hover { background: #2ea043; }
.controls button.secondary { background: #30363d; }
.controls button.secondary:hover { background: #3d444d; }
.summary-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px; }
.card { background: #1c1f26; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
.card .label { font-size: 11px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
.card .value { font-size: 28px; font-weight: 700; color: #fff; }
.card .unit { font-size: 13px; color: #8b949e; font-weight: 400; }
.section { margin-bottom: 28px; }
.section h2 { font-size: 15px; font-weight: 600; margin-bottom: 10px; color: #c9d1d9; display: flex; align-items: center; gap: 8px; }
.section h2 .badge { background: #30363d; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 500; color: #8b949e; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th { text-align: left; padding: 8px 12px; border-bottom: 1px solid #30363d; color: #8b949e; font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; }
thead th.num { text-align: right; }
tbody td { padding: 8px 12px; border-bottom: 1px solid #1c1f26; }
tbody td.num { text-align: right; font-variant-numeric: tabular-nums; }
tbody tr:hover { background: #1c1f26; }
tbody td.endpoint { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; color: #58a6ff; }
.bar-cell { position: relative; }
.bar-bg { position: absolute; left: 0; top: 2px; bottom: 2px; border-radius: 3px; opacity: 0.15; }
.bar-db { background: #f0883e; }
.bar-app { background: #58a6ff; }
.bar-wrap { display: flex; height: 18px; border-radius: 3px; overflow: hidden; min-width: 80px; }
.bar-seg-db { background: #f0883e; height: 100%; }
.bar-seg-app { background: #58a6ff; height: 100%; }
.legend { display: flex; gap: 16px; font-size: 11px; color: #8b949e; margin-bottom: 12px; }
.legend span { display: flex; align-items: center; gap: 4px; }
.legend .dot { width: 10px; height: 10px; border-radius: 2px; }
.legend .dot.db { background: #f0883e; }
.legend .dot.app { background: #58a6ff; }
.status { padding: 8px 12px; background: #1c1f26; border-radius: 6px; font-size: 12px; color: #8b949e; margin-bottom: 16px; }
.status.error { border: 1px solid #da3633; color: #f85149; }
.status.loading { border: 1px solid #30363d; }
.tag { display: inline-block; padding: 1px 6px; border-radius: 4px; font-size: 11px; font-weight: 500; }
.tag.fast { background: #23863620; color: #3fb950; }
.tag.ok { background: #d29b0020; color: #d29922; }
.tag.slow { background: #da363320; color: #f85149; }
.auto-refresh { display: flex; align-items: center; gap: 6px; margin-left: auto; }
.auto-refresh input[type=checkbox] { accent-color: #238636; }
.timestamp { font-size: 11px; color: #484f58; }
</style>
</head>
<body>
<h1>API Performance Dashboard</h1>
<div class="controls">
<label>Time range:
<select id="hours">
<option value="1">Last 1 hour</option>
<option value="6">Last 6 hours</option>
<option value="24" selected>Last 24 hours</option>
<option value="72">Last 3 days</option>
<option value="168">Last 7 days</option>
<option value="720">Last 30 days</option>
</select>
</label>
<label>Rows:
<input type="number" id="limit" value="20" min="5" max="100" style="width:60px">
</label>
<button onclick="loadAll()">Refresh</button>
<div class="auto-refresh">
<input type="checkbox" id="autoRefresh">
<label for="autoRefresh" style="font-size:12px;color:#8b949e;cursor:pointer">Auto-refresh 30s</label>
</div>
<span class="timestamp" id="lastUpdated"></span>
</div>
<div id="status" class="status loading">Loading...</div>
<div id="summarySection" class="section" style="display:none">
<div class="summary-cards" id="summaryCards"></div>
</div>
<div class="legend">
<span><span class="dot db"></span> DB time</span>
<span><span class="dot app"></span> App time</span>
</div>
<div id="countSection" class="section" style="display:none">
<h2>Top Endpoints by Volume <span class="badge" id="countBadge"></span></h2>
<div style="overflow-x:auto"><table id="countTable"><thead></thead><tbody></tbody></table></div>
</div>
<div id="latencySection" class="section" style="display:none">
<h2>Top Endpoints by Latency <span class="badge" id="latencyBadge"></span></h2>
<div style="overflow-x:auto"><table id="latencyTable"><thead></thead><tbody></tbody></table></div>
</div>
<div id="slowSection" class="section" style="display:none">
<h2>Slowest Individual Requests <span class="badge" id="slowBadge"></span></h2>
<div style="overflow-x:auto"><table id="slowTable"><thead></thead><tbody></tbody></table></div>
</div>
<script>
const API = 'perf.cfm';
let refreshTimer = null;
function qs(s) { return document.querySelector(s); }
function fmt(n) { return n == null ? '-' : Number(n).toLocaleString(); }
function fmtMs(n) {
if (n == null) return '-';
n = Number(n);
if (n < 100) return '<span class="tag fast">' + n + 'ms</span>';
if (n < 500) return '<span class="tag ok">' + n + 'ms</span>';
return '<span class="tag slow">' + n + 'ms</span>';
}
function fmtBytes(n) {
if (!n) return '-';
n = Number(n);
if (n < 1024) return n + ' B';
if (n < 1048576) return (n / 1024).toFixed(1) + ' KB';
return (n / 1048576).toFixed(1) + ' MB';
}
function pct(part, total) { return total > 0 ? Math.round(part / total * 100) : 0; }
function timeBar(dbMs, appMs) {
var total = (dbMs || 0) + (appMs || 0);
if (total === 0) return '';
var dbPct = pct(dbMs, total);
var appPct = 100 - dbPct;
return '<div class="bar-wrap" title="DB: ' + dbMs + 'ms / App: ' + appMs + 'ms">'
+ '<div class="bar-seg-db" style="width:' + dbPct + '%"></div>'
+ '<div class="bar-seg-app" style="width:' + appPct + '%"></div>'
+ '</div>';
}
function endpointName(ep) {
return ep.replace(/^\/api\//, '');
}
async function fetchView(view) {
var h = qs('#hours').value;
var l = qs('#limit').value;
var r = await fetch(API + '?view=' + view + '&hours=' + h + '&limit=' + l);
return r.json();
}
async function loadAll() {
qs('#status').className = 'status loading';
qs('#status').textContent = 'Loading...';
qs('#status').style.display = '';
try {
var [summary, count, latency, slow] = await Promise.all([
fetchView('summary'), fetchView('count'), fetchView('latency'), fetchView('slow')
]);
if (!summary.OK) throw new Error(summary.MESSAGE || summary.ERROR);
qs('#status').style.display = 'none';
// Summary cards
var s = summary.DATA;
qs('#summaryCards').innerHTML =
card('Total Requests', fmt(s.TotalRequests), '') +
card('Unique Endpoints', fmt(s.UniqueEndpoints), '') +
card('Avg Latency', s.OverallAvgMs || 0, 'ms') +
card('Max Latency', s.OverallMaxMs || 0, 'ms') +
card('Avg DB Time', s.OverallAvgDbMs || 0, 'ms') +
card('Avg App Time', s.OverallAvgAppMs || 0, 'ms') +
card('Avg Queries', s.OverallAvgQueries || 0, '/req') +
card('Data Since', s.FirstLog || 'none', '');
qs('#summarySection').style.display = '';
// Count table
renderCountTable(count.DATA || []);
// Latency table
renderLatencyTable(latency.DATA || []);
// Slow table
renderSlowTable(slow.DATA || []);
qs('#lastUpdated').textContent = 'Updated ' + new Date().toLocaleTimeString();
} catch (e) {
qs('#status').className = 'status error';
qs('#status').textContent = 'Error: ' + e.message;
qs('#status').style.display = '';
}
}
function card(label, value, unit) {
return '<div class="card"><div class="label">' + label + '</div><div class="value">' + value + ' <span class="unit">' + unit + '</span></div></div>';
}
function renderCountTable(data) {
if (!data.length) { qs('#countSection').style.display = 'none'; return; }
qs('#countBadge').textContent = data.length + ' endpoints';
var maxCalls = Math.max(...data.map(r => r.Calls));
var html = '<thead><tr><th>Endpoint</th><th class="num">Calls</th><th class="num">Avg</th><th class="num">DB</th><th class="num">App</th><th>DB / App Split</th><th class="num">Max</th><th class="num">Queries</th><th class="num">Avg Size</th></tr></thead><tbody>';
data.forEach(function(r) {
html += '<tr>'
+ '<td class="endpoint">' + endpointName(r.Endpoint) + '</td>'
+ '<td class="num">' + fmt(r.Calls) + '</td>'
+ '<td class="num">' + fmtMs(r.AvgMs) + '</td>'
+ '<td class="num">' + fmt(r.AvgDbMs) + 'ms</td>'
+ '<td class="num">' + fmt(r.AvgAppMs) + 'ms</td>'
+ '<td>' + timeBar(r.AvgDbMs, r.AvgAppMs) + '</td>'
+ '<td class="num">' + fmtMs(r.MaxMs) + '</td>'
+ '<td class="num">' + r.AvgQueries + '</td>'
+ '<td class="num">' + fmtBytes(r.AvgBytes) + '</td>'
+ '</tr>';
});
html += '</tbody>';
qs('#countTable').innerHTML = html;
qs('#countSection').style.display = '';
}
function renderLatencyTable(data) {
if (!data.length) { qs('#latencySection').style.display = 'none'; return; }
qs('#latencyBadge').textContent = data.length + ' endpoints';
var html = '<thead><tr><th>Endpoint</th><th class="num">Calls</th><th class="num">Avg</th><th class="num">DB</th><th class="num">App</th><th>DB / App Split</th><th class="num">Max</th><th class="num">Queries</th></tr></thead><tbody>';
data.forEach(function(r) {
html += '<tr>'
+ '<td class="endpoint">' + endpointName(r.Endpoint) + '</td>'
+ '<td class="num">' + fmt(r.Calls) + '</td>'
+ '<td class="num">' + fmtMs(r.AvgMs) + '</td>'
+ '<td class="num">' + fmt(r.AvgDbMs) + 'ms</td>'
+ '<td class="num">' + fmt(r.AvgAppMs) + 'ms</td>'
+ '<td>' + timeBar(r.AvgDbMs, r.AvgAppMs) + '</td>'
+ '<td class="num">' + fmtMs(r.MaxMs) + '</td>'
+ '<td class="num">' + r.AvgQueries + '</td>'
+ '</tr>';
});
html += '</tbody>';
qs('#latencyTable').innerHTML = html;
qs('#latencySection').style.display = '';
}
function renderSlowTable(data) {
if (!data.length) { qs('#slowSection').style.display = 'none'; return; }
qs('#slowBadge').textContent = data.length + ' requests';
var html = '<thead><tr><th>Endpoint</th><th class="num">Total</th><th class="num">DB</th><th class="num">App</th><th>DB / App Split</th><th class="num">Queries</th><th class="num">Size</th><th class="num">Biz</th><th>When</th></tr></thead><tbody>';
data.forEach(function(r) {
html += '<tr>'
+ '<td class="endpoint">' + endpointName(r.Endpoint) + '</td>'
+ '<td class="num">' + fmtMs(r.TotalMs) + '</td>'
+ '<td class="num">' + fmt(r.DbMs) + 'ms</td>'
+ '<td class="num">' + fmt(r.AppMs) + 'ms</td>'
+ '<td>' + timeBar(r.DbMs, r.AppMs) + '</td>'
+ '<td class="num">' + r.QueryCount + '</td>'
+ '<td class="num">' + fmtBytes(r.ResponseBytes) + '</td>'
+ '<td class="num">' + (r.BusinessID || '-') + '</td>'
+ '<td style="font-size:11px;color:#8b949e;white-space:nowrap">' + (r.LoggedAt || '') + '</td>'
+ '</tr>';
});
html += '</tbody>';
qs('#slowTable').innerHTML = html;
qs('#slowSection').style.display = '';
}
// Auto-refresh
qs('#autoRefresh').addEventListener('change', function() {
if (this.checked) {
refreshTimer = setInterval(loadAll, 30000);
} else {
clearInterval(refreshTimer);
refreshTimer = null;
}
});
// Load on page open
loadAll();
</script>
</body>
</html>

179
api/admin/perf.cfm Normal file
View file

@ -0,0 +1,179 @@
<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(payload) {
writeOutput(serializeJSON(payload));
abort;
}
// Localhost-only protection
remoteAddr = cgi.REMOTE_ADDR;
if (remoteAddr != "127.0.0.1" && remoteAddr != "::1" && remoteAddr != "0:0:0:0:0:0:0:1" && remoteAddr != "10.10.0.2") {
apiAbort({ "OK": false, "ERROR": "forbidden" });
}
try {
// Parse parameters
view = structKeyExists(url, "view") ? lcase(url.view) : "count";
hours = structKeyExists(url, "hours") ? val(url.hours) : 24;
if (hours <= 0 || hours > 720) hours = 24;
limitRows = structKeyExists(url, "limit") ? val(url.limit) : 20;
if (limitRows <= 0 || limitRows > 100) limitRows = 20;
// Flush any buffered metrics first
flushPerfBuffer();
response = { "OK": true, "VIEW": view, "HOURS": hours };
if (view == "count") {
// Top endpoints by call count
q = queryExecute("
SELECT
Endpoint,
COUNT(*) as Calls,
ROUND(AVG(TotalMs)) as AvgMs,
ROUND(AVG(DbMs)) as AvgDbMs,
ROUND(AVG(AppMs)) as AvgAppMs,
MAX(TotalMs) as MaxMs,
ROUND(AVG(QueryCount), 1) as AvgQueries,
ROUND(AVG(ResponseBytes)) as AvgBytes
FROM ApiPerfLogs
WHERE LoggedAt > DATE_SUB(NOW(), INTERVAL :hours HOUR)
GROUP BY Endpoint
ORDER BY Calls DESC
LIMIT :lim
", {
hours: { value: hours, cfsqltype: "cf_sql_integer" },
lim: { value: limitRows, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
rows = [];
for (row in q) {
arrayAppend(rows, {
"Endpoint": row.Endpoint,
"Calls": row.Calls,
"AvgMs": row.AvgMs,
"AvgDbMs": row.AvgDbMs,
"AvgAppMs": row.AvgAppMs,
"MaxMs": row.MaxMs,
"AvgQueries": row.AvgQueries,
"AvgBytes": row.AvgBytes
});
}
response["DATA"] = rows;
} else if (view == "latency") {
// Top endpoints by average latency
q = queryExecute("
SELECT
Endpoint,
COUNT(*) as Calls,
ROUND(AVG(TotalMs)) as AvgMs,
ROUND(AVG(DbMs)) as AvgDbMs,
ROUND(AVG(AppMs)) as AvgAppMs,
MAX(TotalMs) as MaxMs,
ROUND(AVG(QueryCount), 1) as AvgQueries
FROM ApiPerfLogs
WHERE LoggedAt > DATE_SUB(NOW(), INTERVAL :hours HOUR)
GROUP BY Endpoint
HAVING Calls >= 3
ORDER BY AvgMs DESC
LIMIT :lim
", {
hours: { value: hours, cfsqltype: "cf_sql_integer" },
lim: { value: limitRows, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
rows = [];
for (row in q) {
arrayAppend(rows, {
"Endpoint": row.Endpoint,
"Calls": row.Calls,
"AvgMs": row.AvgMs,
"AvgDbMs": row.AvgDbMs,
"AvgAppMs": row.AvgAppMs,
"MaxMs": row.MaxMs,
"AvgQueries": row.AvgQueries
});
}
response["DATA"] = rows;
} else if (view == "slow") {
// Slowest individual requests
q = queryExecute("
SELECT Endpoint, TotalMs, DbMs, AppMs, QueryCount, ResponseBytes,
BusinessID, UserID, LoggedAt
FROM ApiPerfLogs
WHERE LoggedAt > DATE_SUB(NOW(), INTERVAL :hours HOUR)
ORDER BY TotalMs DESC
LIMIT :lim
", {
hours: { value: hours, cfsqltype: "cf_sql_integer" },
lim: { value: limitRows, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
rows = [];
for (row in q) {
arrayAppend(rows, {
"Endpoint": row.Endpoint,
"TotalMs": row.TotalMs,
"DbMs": row.DbMs,
"AppMs": row.AppMs,
"QueryCount": row.QueryCount,
"ResponseBytes": row.ResponseBytes,
"BusinessID": row.BusinessID,
"UserID": row.UserID,
"LoggedAt": dateTimeFormat(row.LoggedAt, "yyyy-mm-dd HH:nn:ss")
});
}
response["DATA"] = rows;
} else if (view == "summary") {
// Overall summary stats
q = queryExecute("
SELECT
COUNT(*) as TotalRequests,
COUNT(DISTINCT Endpoint) as UniqueEndpoints,
ROUND(AVG(TotalMs)) as OverallAvgMs,
MAX(TotalMs) as OverallMaxMs,
ROUND(AVG(DbMs)) as OverallAvgDbMs,
ROUND(AVG(AppMs)) as OverallAvgAppMs,
ROUND(AVG(QueryCount), 1) as OverallAvgQueries,
MIN(LoggedAt) as FirstLog,
MAX(LoggedAt) as LastLog
FROM ApiPerfLogs
WHERE LoggedAt > DATE_SUB(NOW(), INTERVAL :hours HOUR)
", {
hours: { value: hours, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
response["DATA"] = {
"TotalRequests": q.TotalRequests,
"UniqueEndpoints": q.UniqueEndpoints,
"OverallAvgMs": q.OverallAvgMs,
"OverallMaxMs": q.OverallMaxMs,
"OverallAvgDbMs": q.OverallAvgDbMs,
"OverallAvgAppMs": q.OverallAvgAppMs,
"OverallAvgQueries": q.OverallAvgQueries,
"FirstLog": isDate(q.FirstLog) ? dateTimeFormat(q.FirstLog, "yyyy-mm-dd HH:nn:ss") : "",
"LastLog": isDate(q.LastLog) ? dateTimeFormat(q.LastLog, "yyyy-mm-dd HH:nn:ss") : ""
};
} else {
apiAbort({ "OK": false, "ERROR": "invalid_view", "MESSAGE": "Use ?view=count|latency|slow|summary" });
}
apiAbort(response);
} catch (any e) {
apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": e.message,
"DETAIL": e.detail
});
}
</cfscript>

View file

@ -0,0 +1,37 @@
<cfsetting showdebugoutput="false">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
// Add TaskTypeID column to QuickTaskTemplates table if it doesn't exist
try {
// Check if column exists
qCheck = queryExecute("
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'payfrit'
AND TABLE_NAME = 'QuickTaskTemplates'
AND COLUMN_NAME = 'TaskTypeID'
", [], { datasource: "payfrit" });
if (qCheck.recordCount == 0) {
queryExecute("
ALTER TABLE QuickTaskTemplates
ADD COLUMN TaskTypeID INT NULL AFTER TaskCategoryID
", [], { datasource: "payfrit" });
writeOutput(serializeJSON({
"OK": true,
"MESSAGE": "Column TaskTypeID added to QuickTaskTemplates"
}));
} else {
writeOutput(serializeJSON({
"OK": true,
"MESSAGE": "Column already exists"
}));
}
} catch (any e) {
writeOutput(serializeJSON({
"OK": false,
"ERROR": e.message
}));
}
</cfscript>

View file

@ -0,0 +1,21 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
// One-time cleanup: delete test tasks and reset
try {
// Delete tasks 30, 31, 32 (test tasks with bad data)
queryExecute("DELETE FROM Tasks WHERE ID IN (30, 31, 32)", [], { datasource: "payfrit" });
writeOutput(serializeJSON({
"OK": true,
"MESSAGE": "Cleanup complete - deleted test tasks"
}));
} catch (any e) {
writeOutput(serializeJSON({
"OK": false,
"ERROR": e.message
}));
}
</cfscript>

View file

@ -50,14 +50,13 @@ try {
// Get template details
qTemplate = queryExecute("
SELECT
QuickTaskTemplateTitle as Title,
QuickTaskTemplateDetails as Details,
QuickTaskTemplateCategoryID as CategoryID,
QuickTaskTemplateTypeID as TypeID
Title as Title,
Details as Details,
TaskCategoryID as CategoryID
FROM QuickTaskTemplates
WHERE QuickTaskTemplateID = :id
AND QuickTaskTemplateBusinessID = :businessID
AND QuickTaskTemplateIsActive = 1
WHERE ID = :id
AND BusinessID = :businessID
AND IsActive = 1
", {
id: { value: templateID, cfsqltype: "cf_sql_integer" },
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
@ -67,24 +66,21 @@ try {
apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Template not found" });
}
// Create the task
// Create the task (ClaimedByUserID=0 means unclaimed/pending)
queryExecute("
INSERT INTO Tasks (
TaskBusinessID, TaskCategoryID, TaskTypeID,
TaskTitle, TaskDetails, TaskStatusID, TaskAddedOn,
TaskSourceType, TaskSourceID
BusinessID, CategoryID, TaskTypeID,
Title, Details, CreatedOn, ClaimedByUserID
) VALUES (
:businessID, :categoryID, :typeID,
:title, :details, 0, NOW(),
'quicktask', :templateID
:title, :details, NOW(), 0
)
", {
businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
categoryID: { value: qTemplate.CategoryID, cfsqltype: "cf_sql_integer", null: isNull(qTemplate.CategoryID) },
typeID: { value: qTemplate.TypeID, cfsqltype: "cf_sql_integer", null: isNull(qTemplate.TypeID) },
typeID: { value: 0, cfsqltype: "cf_sql_integer" },
title: { value: qTemplate.Title, cfsqltype: "cf_sql_varchar" },
details: { value: qTemplate.Details, cfsqltype: "cf_sql_longvarchar", null: isNull(qTemplate.Details) },
templateID: { value: templateID, cfsqltype: "cf_sql_integer" }
details: { value: qTemplate.Details, cfsqltype: "cf_sql_longvarchar", null: isNull(qTemplate.Details) }
}, { datasource: "payfrit" });
qNew = queryExecute("SELECT LAST_INSERT_ID() as newID", [], { datasource: "payfrit" });

View file

@ -0,0 +1,39 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
try {
q = queryExecute("
SELECT ID, Title, Details, TaskCategoryID, ClaimedByUserID, CompletedOn, CreatedOn
FROM Tasks
WHERE BusinessID = 47
ORDER BY ID DESC
LIMIT 20
", [], { datasource: "payfrit" });
tasks = [];
for (row in q) {
arrayAppend(tasks, {
"TaskID": row.ID,
"Title": row.Title,
"Details": isNull(row.Details) ? "" : row.Details,
"CategoryID": row.ID,
"ClaimedByUserID": row.ClaimedByUserID,
"CompletedOn": isNull(row.CompletedOn) ? "" : dateTimeFormat(row.CompletedOn, "yyyy-mm-dd HH:nn:ss"),
"AddedOn": isNull(row.CreatedOn) ? "" : dateTimeFormat(row.CreatedOn, "yyyy-mm-dd HH:nn:ss")
});
}
writeOutput(serializeJSON({
"OK": true,
"COUNT": arrayLen(tasks),
"TASKS": tasks
}));
} catch (any e) {
writeOutput(serializeJSON({
"OK": false,
"ERROR": e.message
}));
}
</cfscript>

View file

@ -50,7 +50,7 @@ try {
// Verify template exists and belongs to this business
qCheck = queryExecute("
SELECT QuickTaskTemplateID FROM QuickTaskTemplates
WHERE QuickTaskTemplateID = :id AND QuickTaskTemplateBusinessID = :businessID
WHERE ID = :id AND BusinessID = :businessID
", {
id: { value: templateID, cfsqltype: "cf_sql_integer" },
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
@ -62,8 +62,8 @@ try {
// Soft delete by setting IsActive to 0
queryExecute("
UPDATE QuickTaskTemplates SET QuickTaskTemplateIsActive = 0
WHERE QuickTaskTemplateID = :id
UPDATE QuickTaskTemplates SET IsActive = 0
WHERE ID = :id
", {
id: { value: templateID, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });

View file

@ -45,23 +45,22 @@ try {
// Get quick task templates for this business
q = queryExecute("
SELECT
qt.QuickTaskTemplateID,
qt.QuickTaskTemplateName as Name,
qt.QuickTaskTemplateCategoryID as CategoryID,
qt.QuickTaskTemplateTypeID as TypeID,
qt.QuickTaskTemplateTitle as Title,
qt.QuickTaskTemplateDetails as Details,
qt.QuickTaskTemplateIcon as Icon,
qt.QuickTaskTemplateColor as Color,
qt.QuickTaskTemplateSortOrder as SortOrder,
qt.QuickTaskTemplateIsActive as IsActive,
tc.TaskCategoryName as CategoryName,
tc.TaskCategoryColor as CategoryColor
qt.ID,
qt.Name as Name,
qt.TaskCategoryID as CategoryID,
qt.Title as Title,
qt.Details as Details,
qt.Icon as Icon,
qt.Color as Color,
qt.SortOrder as SortOrder,
qt.IsActive as IsActive,
tc.Name as Name,
tc.Color as CategoryColor
FROM QuickTaskTemplates qt
LEFT JOIN TaskCategories tc ON qt.QuickTaskTemplateCategoryID = tc.TaskCategoryID
WHERE qt.QuickTaskTemplateBusinessID = :businessID
AND qt.QuickTaskTemplateIsActive = 1
ORDER BY qt.QuickTaskTemplateSortOrder, qt.QuickTaskTemplateID
LEFT JOIN TaskCategories tc ON qt.TaskCategoryID = tc.ID
WHERE qt.BusinessID = :businessID
AND qt.IsActive = 1
ORDER BY qt.SortOrder, qt.ID
", {
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
@ -69,17 +68,16 @@ try {
templates = [];
for (row in q) {
arrayAppend(templates, {
"QuickTaskTemplateID": row.QuickTaskTemplateID,
"QuickTaskTemplateID": row.ID,
"Name": row.Name,
"CategoryID": isNull(row.CategoryID) ? "" : row.CategoryID,
"TypeID": isNull(row.TypeID) ? "" : row.TypeID,
"Title": row.Title,
"Details": isNull(row.Details) ? "" : row.Details,
"Icon": isNull(row.Icon) ? "add_box" : row.Icon,
"Color": isNull(row.Color) ? "##6366f1" : row.Color,
"SortOrder": row.SortOrder,
"IsActive": row.IsActive,
"CategoryName": isNull(row.CategoryName) ? "" : row.CategoryName,
"Name": isNull(row.Name) ? "" : row.Name,
"CategoryColor": isNull(row.CategoryColor) ? "" : row.CategoryColor
});
}

View file

@ -0,0 +1,20 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
try {
// Delete all Quick Task templates for business 1
queryExecute("DELETE FROM QuickTaskTemplates WHERE BusinessID = 1", [], { datasource: "payfrit" });
writeOutput(serializeJSON({
"OK": true,
"MESSAGE": "All Quick Task templates purged"
}));
} catch (any e) {
writeOutput(serializeJSON({
"OK": false,
"ERROR": e.message
}));
}
</cfscript>

View file

@ -5,7 +5,7 @@
<cfscript>
// Create or update a quick task template
// Input: BusinessID (required), QuickTaskTemplateID (optional - for update),
// Name, Title, Details, CategoryID, TypeID, Icon, Color
// Name, Title, Details, CategoryID, Icon, Color
// Output: { OK: true, TEMPLATE_ID: int }
function apiAbort(required struct payload) {
@ -46,8 +46,12 @@ try {
templateName = structKeyExists(data, "Name") ? trim(toString(data.Name)) : "";
templateTitle = structKeyExists(data, "Title") ? trim(toString(data.Title)) : "";
templateDetails = structKeyExists(data, "Details") ? trim(toString(data.Details)) : "";
categoryID = structKeyExists(data, "CategoryID") && isNumeric(data.CategoryID) && data.CategoryID > 0 ? int(data.CategoryID) : javaCast("null", "");
typeID = structKeyExists(data, "TypeID") && isNumeric(data.TypeID) && data.TypeID > 0 ? int(data.TypeID) : javaCast("null", "");
hasCategory = false;
catID = 0;
if (structKeyExists(data, "CategoryID") && isNumeric(data.CategoryID) && data.CategoryID > 0) {
catID = int(data.CategoryID);
hasCategory = true;
}
templateIcon = structKeyExists(data, "Icon") && len(trim(data.Icon)) ? trim(toString(data.Icon)) : "add_box";
templateColor = structKeyExists(data, "Color") && len(trim(data.Color)) ? trim(toString(data.Color)) : "##6366f1";
@ -58,12 +62,15 @@ try {
if (!len(templateTitle)) {
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "Title is required" });
}
if (!hasCategory) {
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "Please select a category" });
}
if (templateID > 0) {
// UPDATE existing template
qCheck = queryExecute("
SELECT QuickTaskTemplateID FROM QuickTaskTemplates
WHERE QuickTaskTemplateID = :id AND QuickTaskTemplateBusinessID = :businessID
WHERE ID = :id AND BusinessID = :businessID
", {
id: { value: templateID, cfsqltype: "cf_sql_integer" },
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
@ -75,20 +82,18 @@ try {
queryExecute("
UPDATE QuickTaskTemplates SET
QuickTaskTemplateName = :name,
QuickTaskTemplateTitle = :title,
QuickTaskTemplateDetails = :details,
QuickTaskTemplateCategoryID = :categoryID,
QuickTaskTemplateTypeID = :typeID,
QuickTaskTemplateIcon = :icon,
QuickTaskTemplateColor = :color
WHERE QuickTaskTemplateID = :id
Name = :name,
Title = :title,
Details = :details,
TaskCategoryID = :categoryID,
Icon = :icon,
Color = :color
WHERE ID = :id
", {
name: { value: templateName, cfsqltype: "cf_sql_varchar" },
title: { value: templateTitle, cfsqltype: "cf_sql_varchar" },
details: { value: templateDetails, cfsqltype: "cf_sql_longvarchar", null: !len(templateDetails) },
categoryID: { value: categoryID, cfsqltype: "cf_sql_integer", null: isNull(categoryID) },
typeID: { value: typeID, cfsqltype: "cf_sql_integer", null: isNull(typeID) },
categoryID: { value: catID, cfsqltype: "cf_sql_integer", null: !hasCategory },
icon: { value: templateIcon, cfsqltype: "cf_sql_varchar" },
color: { value: templateColor, cfsqltype: "cf_sql_varchar" },
id: { value: templateID, cfsqltype: "cf_sql_integer" }
@ -104,8 +109,8 @@ try {
// INSERT new template
// Get next sort order
qSort = queryExecute("
SELECT COALESCE(MAX(QuickTaskTemplateSortOrder), 0) + 1 as nextSort
FROM QuickTaskTemplates WHERE QuickTaskTemplateBusinessID = :businessID
SELECT COALESCE(MAX(SortOrder), 0) + 1 as nextSort
FROM QuickTaskTemplates WHERE BusinessID = :businessID
", {
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
@ -114,19 +119,18 @@ try {
queryExecute("
INSERT INTO QuickTaskTemplates (
QuickTaskTemplateBusinessID, QuickTaskTemplateName, QuickTaskTemplateTitle,
QuickTaskTemplateDetails, QuickTaskTemplateCategoryID, QuickTaskTemplateTypeID,
QuickTaskTemplateIcon, QuickTaskTemplateColor, QuickTaskTemplateSortOrder
BusinessID, Name, Title,
Details, TaskCategoryID,
Icon, Color, SortOrder
) VALUES (
:businessID, :name, :title, :details, :categoryID, :typeID, :icon, :color, :sortOrder
:businessID, :name, :title, :details, :categoryID, :icon, :color, :sortOrder
)
", {
businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
name: { value: templateName, cfsqltype: "cf_sql_varchar" },
title: { value: templateTitle, cfsqltype: "cf_sql_varchar" },
details: { value: templateDetails, cfsqltype: "cf_sql_longvarchar", null: !len(templateDetails) },
categoryID: { value: categoryID, cfsqltype: "cf_sql_integer", null: isNull(categoryID) },
typeID: { value: typeID, cfsqltype: "cf_sql_integer", null: isNull(typeID) },
categoryID: { value: catID, cfsqltype: "cf_sql_integer", null: !hasCategory },
icon: { value: templateIcon, cfsqltype: "cf_sql_varchar" },
color: { value: templateColor, cfsqltype: "cf_sql_varchar" },
sortOrder: { value: nextSort, cfsqltype: "cf_sql_integer" }
@ -145,7 +149,10 @@ try {
apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": e.message
"MESSAGE": e.message,
"DETAIL": structKeyExists(e, "detail") ? e.detail : "none",
"TYPE": structKeyExists(e, "type") ? e.type : "none",
"TAG": (structKeyExists(e, "tagContext") && isArray(e.tagContext) && arrayLen(e.tagContext) > 0) ? serializeJSON(e.tagContext[1]) : "none"
});
}
</cfscript>

View file

@ -15,20 +15,20 @@ try {
// Create QuickTaskTemplates table
queryExecute("
CREATE TABLE IF NOT EXISTS QuickTaskTemplates (
QuickTaskTemplateID INT AUTO_INCREMENT PRIMARY KEY,
QuickTaskTemplateBusinessID INT NOT NULL,
QuickTaskTemplateName VARCHAR(100) NOT NULL,
QuickTaskTemplateCategoryID INT NULL,
QuickTaskTemplateTypeID INT NULL,
QuickTaskTemplateTitle VARCHAR(255) NOT NULL,
QuickTaskTemplateDetails TEXT NULL,
QuickTaskTemplateIcon VARCHAR(30) DEFAULT 'add_box',
QuickTaskTemplateColor VARCHAR(20) DEFAULT '##6366f1',
QuickTaskTemplateSortOrder INT DEFAULT 0,
QuickTaskTemplateIsActive BIT(1) DEFAULT b'1',
QuickTaskTemplateCreatedOn DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_business_active (QuickTaskTemplateBusinessID, QuickTaskTemplateIsActive),
INDEX idx_sort (QuickTaskTemplateBusinessID, QuickTaskTemplateSortOrder)
ID INT AUTO_INCREMENT PRIMARY KEY,
BusinessID INT NOT NULL,
Name VARCHAR(100) NOT NULL,
TaskCategoryID INT NULL,
TaskTypeID INT NULL,
Title VARCHAR(255) NOT NULL,
Details TEXT NULL,
Icon VARCHAR(30) DEFAULT 'add_box',
Color VARCHAR(20) DEFAULT '##6366f1',
SortOrder INT DEFAULT 0,
IsActive BIT(1) DEFAULT b'1',
CreatedOn DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_business_active (BusinessID, IsActive),
INDEX idx_sort (BusinessID, SortOrder)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
", [], { datasource: "payfrit" });

View file

@ -5,42 +5,42 @@
<cfscript>
businessId = 27; // Big Dean's
// Categories are items with ItemParentItemID=0 AND ItemIsCollapsible=0
// Modifier templates are items with ItemParentItemID=0 AND ItemIsCollapsible=1
// Categories are items with ParentItemID=0 AND IsCollapsible=0
// Modifier templates are items with ParentItemID=0 AND IsCollapsible=1
// Menu items are children of categories
// Modifiers are children of menu items or modifier templates
// Get category IDs (NOT modifier templates)
categoryIds = queryExecute("
SELECT ItemID
SELECT ID
FROM Items
WHERE ItemBusinessID = :bizId
AND ItemParentItemID = 0
AND ItemIsCollapsible = 0
WHERE BusinessID = :bizId
AND ParentItemID = 0
AND IsCollapsible = 0
", { bizId: businessId }, { datasource: "payfrit" });
catIdList = "";
for (cat in categoryIds) {
catIdList = listAppend(catIdList, cat.ItemID);
catIdList = listAppend(catIdList, cat.ID);
}
// Now get actual menu items (direct children of categories)
// Exclude items that are template options (their parent is a collapsible modifier group)
items = queryExecute("
SELECT i.ItemID, i.ItemName
SELECT i.ID, i.Name
FROM Items i
WHERE i.ItemBusinessID = :bizId
AND i.ItemParentItemID IN (#catIdList#)
AND i.ItemIsCollapsible = 0
WHERE i.BusinessID = :bizId
AND i.ParentItemID IN (#catIdList#)
AND i.IsCollapsible = 0
AND NOT EXISTS (
SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = i.ItemID
SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = i.ID
)
", { bizId: businessId }, { datasource: "payfrit" });
updated = [];
for (item in items) {
itemName = lcase(item.ItemName);
itemName = lcase(item.Name);
newPrice = 0;
// Drinks - $3-6
@ -99,13 +99,13 @@ for (item in items) {
queryExecute("
UPDATE Items
SET ItemPrice = :price
SET Price = :price
WHERE ItemID = :itemId
", { price: newPrice, itemId: item.ItemID }, { datasource: "payfrit" });
", { price: newPrice, itemId: item.ID }, { datasource: "payfrit" });
arrayAppend(updated, {
"ItemID": item.ItemID,
"ItemName": item.ItemName,
"ItemID": item.ID,
"Name": item.Name,
"NewPrice": newPrice
});
}
@ -113,15 +113,15 @@ for (item in items) {
// Update modifier prices (children of menu items, NOT direct children of categories)
// Modifiers are items whose parent is NOT a category (i.e., parent is a menu item or modifier group)
modifiers = queryExecute("
SELECT ItemID, ItemName
SELECT ID, Name
FROM Items
WHERE ItemBusinessID = :bizId
AND ItemParentItemID > 0
AND ItemParentItemID NOT IN (#catIdList#)
WHERE BusinessID = :bizId
AND ParentItemID > 0
AND ParentItemID NOT IN (#catIdList#)
", { bizId: businessId }, { datasource: "payfrit" });
for (mod in modifiers) {
modName = lcase(mod.ItemName);
modName = lcase(mod.Name);
modPrice = 0;
// Proteins are expensive add-ons
@ -150,7 +150,7 @@ for (mod in modifiers) {
queryExecute("
UPDATE Items
SET ItemPrice = :price
SET Price = :price
WHERE ItemID = :itemId
", { price: modPrice, itemId: mod.ItemID }, { datasource: "payfrit" });
}
@ -158,17 +158,17 @@ for (mod in modifiers) {
// Reset category prices to $0 (shouldn't have prices for reporting)
queryExecute("
UPDATE Items
SET ItemPrice = 0
WHERE ItemBusinessID = :bizId
AND ItemParentItemID = 0
SET Price = 0
WHERE BusinessID = :bizId
AND ParentItemID = 0
", { bizId: businessId }, { datasource: "payfrit" });
// Reset modifier group prices to $0 (only options have prices)
queryExecute("
UPDATE Items
SET ItemPrice = 0
WHERE ItemBusinessID = :bizId
AND ItemIsCollapsible = 1
SET Price = 0
WHERE BusinessID = :bizId
AND IsCollapsible = 1
", { bizId: businessId }, { datasource: "payfrit" });
writeOutput(serializeJSON({

View file

@ -50,7 +50,7 @@ try {
// Verify exists and belongs to business
qCheck = queryExecute("
SELECT ScheduledTaskID FROM ScheduledTaskDefinitions
WHERE ScheduledTaskID = :id AND ScheduledTaskBusinessID = :businessID
WHERE ID = :id AND BusinessID = :businessID
", {
id: { value: taskID, cfsqltype: "cf_sql_integer" },
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
@ -62,7 +62,7 @@ try {
// Hard delete the definition
queryExecute("
DELETE FROM ScheduledTaskDefinitions WHERE ScheduledTaskID = :id
DELETE FROM ScheduledTaskDefinitions WHERE ID = :id
", {
id: { value: taskID, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });

View file

@ -45,23 +45,24 @@ try {
// Get scheduled task definitions for this business
q = queryExecute("
SELECT
st.ScheduledTaskID,
st.ScheduledTaskName as Name,
st.ScheduledTaskCategoryID as CategoryID,
st.ScheduledTaskTypeID as TypeID,
st.ScheduledTaskTitle as Title,
st.ScheduledTaskDetails as Details,
st.ScheduledTaskCronExpression as CronExpression,
st.ScheduledTaskIsActive as IsActive,
st.ScheduledTaskLastRunOn as LastRunOn,
st.ScheduledTaskNextRunOn as NextRunOn,
st.ScheduledTaskCreatedOn as CreatedOn,
tc.TaskCategoryName as CategoryName,
tc.TaskCategoryColor as CategoryColor
st.ID,
st.Name as Name,
st.TaskCategoryID as CategoryID,
st.Title as Title,
st.Details as Details,
st.CronExpression as CronExpression,
COALESCE(st.ScheduleType, 'cron') as ScheduleType,
st.IntervalMinutes as IntervalMinutes,
st.IsActive as IsActive,
st.LastRunOn as LastRunOn,
st.NextRunOn as NextRunOn,
st.CreatedOn as CreatedOn,
tc.Name as Name,
tc.Color as CategoryColor
FROM ScheduledTaskDefinitions st
LEFT JOIN TaskCategories tc ON st.ScheduledTaskCategoryID = tc.TaskCategoryID
WHERE st.ScheduledTaskBusinessID = :businessID
ORDER BY st.ScheduledTaskIsActive DESC, st.ScheduledTaskName
LEFT JOIN TaskCategories tc ON st.TaskCategoryID = tc.ID
WHERE st.BusinessID = :businessID
ORDER BY st.IsActive DESC, st.Name
", {
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
@ -69,18 +70,19 @@ try {
scheduledTasks = [];
for (row in q) {
arrayAppend(scheduledTasks, {
"ScheduledTaskID": row.ScheduledTaskID,
"ScheduledTaskID": row.ID,
"Name": row.Name,
"CategoryID": isNull(row.CategoryID) ? "" : row.CategoryID,
"TypeID": isNull(row.TypeID) ? "" : row.TypeID,
"Title": row.Title,
"Details": isNull(row.Details) ? "" : row.Details,
"CronExpression": row.CronExpression,
"ScheduleType": row.ScheduleType,
"IntervalMinutes": isNull(row.IntervalMinutes) ? "" : row.IntervalMinutes,
"IsActive": row.IsActive ? true : false,
"LastRunOn": isNull(row.LastRunOn) ? "" : dateTimeFormat(row.LastRunOn, "yyyy-mm-dd HH:nn:ss"),
"NextRunOn": isNull(row.NextRunOn) ? "" : dateTimeFormat(row.NextRunOn, "yyyy-mm-dd HH:nn:ss"),
"CreatedOn": dateTimeFormat(row.CreatedOn, "yyyy-mm-dd HH:nn:ss"),
"CategoryName": isNull(row.CategoryName) ? "" : row.CategoryName,
"Name": isNull(row.Name) ? "" : row.Name,
"CategoryColor": isNull(row.CategoryColor) ? "" : row.CategoryColor
});
}

View file

@ -51,12 +51,11 @@ try {
// Get scheduled task definition
qDef = queryExecute("
SELECT
ScheduledTaskTitle as Title,
ScheduledTaskDetails as Details,
ScheduledTaskCategoryID as CategoryID,
ScheduledTaskTypeID as TypeID
Title as Title,
Details as Details,
TaskCategoryID as CategoryID
FROM ScheduledTaskDefinitions
WHERE ScheduledTaskID = :id AND ScheduledTaskBusinessID = :businessID
WHERE ID = :id AND BusinessID = :businessID
", {
id: { value: scheduledTaskID, cfsqltype: "cf_sql_integer" },
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
@ -66,24 +65,21 @@ try {
apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Scheduled task not found" });
}
// Create the task
// Create the task (ClaimedByUserID=0 means unclaimed/pending)
queryExecute("
INSERT INTO Tasks (
TaskBusinessID, TaskCategoryID, TaskTypeID,
TaskTitle, TaskDetails, TaskStatusID, TaskAddedOn,
TaskSourceType, TaskSourceID
BusinessID, CategoryID, TaskTypeID,
Title, Details, CreatedOn, ClaimedByUserID
) VALUES (
:businessID, :categoryID, :typeID,
:title, :details, 0, NOW(),
'scheduled_manual', :scheduledTaskID
:title, :details, NOW(), 0
)
", {
businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
categoryID: { value: qDef.CategoryID, cfsqltype: "cf_sql_integer", null: isNull(qDef.CategoryID) },
typeID: { value: qDef.TypeID, cfsqltype: "cf_sql_integer", null: isNull(qDef.TypeID) },
typeID: { value: 0, cfsqltype: "cf_sql_integer" },
title: { value: qDef.Title, cfsqltype: "cf_sql_varchar" },
details: { value: qDef.Details, cfsqltype: "cf_sql_longvarchar", null: isNull(qDef.Details) },
scheduledTaskID: { value: scheduledTaskID, cfsqltype: "cf_sql_integer" }
details: { value: qDef.Details, cfsqltype: "cf_sql_longvarchar", null: isNull(qDef.Details) }
}, { datasource: "payfrit" });
qNew = queryExecute("SELECT LAST_INSERT_ID() as newID", [], { datasource: "payfrit" });

View file

@ -68,57 +68,65 @@ try {
dueTasks = queryExecute("
SELECT
ScheduledTaskID,
ScheduledTaskBusinessID as BusinessID,
ScheduledTaskCategoryID as CategoryID,
ScheduledTaskTypeID as TypeID,
ScheduledTaskTitle as Title,
ScheduledTaskDetails as Details,
ScheduledTaskCronExpression as CronExpression
BusinessID as BusinessID,
TaskCategoryID as CategoryID,
Title as Title,
Details as Details,
CronExpression as CronExpression,
COALESCE(ScheduleType, 'cron') as ScheduleType,
IntervalMinutes as IntervalMinutes
FROM ScheduledTaskDefinitions
WHERE ScheduledTaskIsActive = 1
AND ScheduledTaskNextRunOn <= NOW()
WHERE IsActive = 1
AND NextRunOn <= NOW()
", {}, { datasource: "payfrit" });
createdTasks = [];
for (task in dueTasks) {
// Create the actual task
// Create the actual task (ClaimedByUserID=0 means unclaimed/pending)
queryExecute("
INSERT INTO Tasks (
TaskBusinessID, TaskCategoryID, TaskTypeID,
TaskTitle, TaskDetails, TaskStatusID, TaskAddedOn,
TaskSourceType, TaskSourceID
BusinessID, CategoryID, TaskTypeID,
Title, Details, CreatedOn, ClaimedByUserID
) VALUES (
:businessID, :categoryID, :typeID,
:title, :details, 0, NOW(),
'scheduled', :scheduledTaskID
:title, :details, NOW(), 0
)
", {
businessID: { value: task.BusinessID, cfsqltype: "cf_sql_integer" },
categoryID: { value: task.CategoryID, cfsqltype: "cf_sql_integer", null: isNull(task.CategoryID) },
typeID: { value: task.TypeID, cfsqltype: "cf_sql_integer", null: isNull(task.TypeID) },
typeID: { value: 0, cfsqltype: "cf_sql_integer" },
title: { value: task.Title, cfsqltype: "cf_sql_varchar" },
details: { value: task.Details, cfsqltype: "cf_sql_longvarchar", null: isNull(task.Details) },
scheduledTaskID: { value: task.ScheduledTaskID, cfsqltype: "cf_sql_integer" }
details: { value: task.Details, cfsqltype: "cf_sql_longvarchar", null: isNull(task.Details) }
}, { datasource: "payfrit" });
qNew = queryExecute("SELECT LAST_INSERT_ID() as newID", [], { datasource: "payfrit" });
// Calculate next run and update the scheduled task
// Calculate next run based on schedule type
if (task.ScheduleType == "interval_after_completion" && !isNull(task.IntervalMinutes) && task.IntervalMinutes > 0) {
// After-completion interval: don't schedule next run until task is completed
// Set to far future (effectively paused until task completion triggers recalculation)
nextRun = javaCast("null", "");
} else if (task.ScheduleType == "interval" && !isNull(task.IntervalMinutes) && task.IntervalMinutes > 0) {
// Fixed interval: next run = NOW + interval minutes
nextRun = dateAdd("n", task.IntervalMinutes, now());
} else {
// Cron-based: use cron parser
nextRun = calculateNextRun(task.CronExpression);
}
queryExecute("
UPDATE ScheduledTaskDefinitions SET
ScheduledTaskLastRunOn = NOW(),
ScheduledTaskNextRunOn = :nextRun
WHERE ScheduledTaskID = :id
LastRunOn = NOW(),
NextRunOn = :nextRun
WHERE ID = :id
", {
nextRun: { value: nextRun, cfsqltype: "cf_sql_timestamp" },
id: { value: task.ScheduledTaskID, cfsqltype: "cf_sql_integer" }
nextRun: { value: nextRun, cfsqltype: "cf_sql_timestamp", null: isNull(nextRun) },
id: { value: task.ID, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
arrayAppend(createdTasks, {
"ScheduledTaskID": task.ScheduledTaskID,
"ScheduledTaskID": task.ID,
"TaskID": qNew.newID,
"BusinessID": task.BusinessID,
"Title": task.Title

View file

@ -5,7 +5,8 @@
<cfscript>
// Create or update a scheduled task definition
// Input: BusinessID (required), ScheduledTaskID (optional - for update),
// Name, Title, Details, CategoryID, TypeID, CronExpression, IsActive
// Name, Title, Details, CategoryID, TypeID, CronExpression, IsActive,
// ScheduleType ('cron' or 'interval'), IntervalMinutes (for interval type)
// Output: { OK: true, SCHEDULED_TASK_ID: int }
function apiAbort(required struct payload) {
@ -108,10 +109,13 @@ try {
taskTitle = structKeyExists(data, "Title") ? trim(toString(data.Title)) : "";
taskDetails = structKeyExists(data, "Details") ? trim(toString(data.Details)) : "";
categoryID = structKeyExists(data, "CategoryID") && isNumeric(data.CategoryID) && data.CategoryID > 0 ? int(data.CategoryID) : javaCast("null", "");
typeID = structKeyExists(data, "TypeID") && isNumeric(data.TypeID) && data.TypeID > 0 ? int(data.TypeID) : javaCast("null", "");
cronExpression = structKeyExists(data, "CronExpression") ? trim(toString(data.CronExpression)) : "";
isActive = structKeyExists(data, "IsActive") ? (data.IsActive ? 1 : 0) : 1;
// New interval scheduling fields
scheduleType = structKeyExists(data, "ScheduleType") ? trim(toString(data.ScheduleType)) : "cron";
intervalMinutes = structKeyExists(data, "IntervalMinutes") && isNumeric(data.IntervalMinutes) ? int(data.IntervalMinutes) : javaCast("null", "");
// Validate required fields
if (!len(taskName)) {
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "Name is required" });
@ -119,24 +123,42 @@ try {
if (!len(taskTitle)) {
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "Title is required" });
}
// Validate based on schedule type
if (scheduleType == "interval" || scheduleType == "interval_after_completion") {
// Interval-based scheduling
if (isNull(intervalMinutes) || intervalMinutes < 1) {
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "IntervalMinutes is required for interval scheduling (minimum 1)" });
}
// Set a placeholder cron expression for interval type
if (!len(cronExpression)) {
cronExpression = "* * * * *";
}
// For NEW tasks: run immediately. For UPDATES: next run = NOW + interval
if (taskID == 0) {
nextRunOn = now(); // Run immediately on first creation
} else {
nextRunOn = dateAdd("n", intervalMinutes, now());
}
} else {
// Cron-based scheduling
if (!len(cronExpression)) {
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "CronExpression is required" });
}
// Validate cron format (5 parts)
cronParts = listToArray(cronExpression, " ");
if (arrayLen(cronParts) != 5) {
apiAbort({ "OK": false, "ERROR": "invalid_cron", "MESSAGE": "Cron expression must have 5 parts: minute hour day month weekday" });
}
// Calculate next run time
// Calculate next run time from cron
nextRunOn = calculateNextRun(cronExpression);
}
if (taskID > 0) {
// UPDATE existing
qCheck = queryExecute("
SELECT ScheduledTaskID FROM ScheduledTaskDefinitions
WHERE ScheduledTaskID = :id AND ScheduledTaskBusinessID = :businessID
WHERE ID = :id AND BusinessID = :businessID
", {
id: { value: taskID, cfsqltype: "cf_sql_integer" },
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
@ -148,22 +170,24 @@ try {
queryExecute("
UPDATE ScheduledTaskDefinitions SET
ScheduledTaskName = :name,
ScheduledTaskTitle = :title,
ScheduledTaskDetails = :details,
ScheduledTaskCategoryID = :categoryID,
ScheduledTaskTypeID = :typeID,
ScheduledTaskCronExpression = :cron,
ScheduledTaskIsActive = :isActive,
ScheduledTaskNextRunOn = :nextRun
WHERE ScheduledTaskID = :id
Name = :name,
Title = :title,
Details = :details,
TaskCategoryID = :categoryID,
CronExpression = :cron,
ScheduleType = :scheduleType,
IntervalMinutes = :intervalMinutes,
IsActive = :isActive,
NextRunOn = :nextRun
WHERE ID = :id
", {
name: { value: taskName, cfsqltype: "cf_sql_varchar" },
title: { value: taskTitle, cfsqltype: "cf_sql_varchar" },
details: { value: taskDetails, cfsqltype: "cf_sql_longvarchar", null: !len(taskDetails) },
categoryID: { value: categoryID, cfsqltype: "cf_sql_integer", null: isNull(categoryID) },
typeID: { value: typeID, cfsqltype: "cf_sql_integer", null: isNull(typeID) },
cron: { value: cronExpression, cfsqltype: "cf_sql_varchar" },
scheduleType: { value: scheduleType, cfsqltype: "cf_sql_varchar" },
intervalMinutes: { value: intervalMinutes, cfsqltype: "cf_sql_integer", null: isNull(intervalMinutes) },
isActive: { value: isActive, cfsqltype: "cf_sql_bit" },
nextRun: { value: nextRunOn, cfsqltype: "cf_sql_timestamp" },
id: { value: taskID, cfsqltype: "cf_sql_integer" }
@ -180,11 +204,12 @@ try {
// INSERT new
queryExecute("
INSERT INTO ScheduledTaskDefinitions (
ScheduledTaskBusinessID, ScheduledTaskName, ScheduledTaskTitle,
ScheduledTaskDetails, ScheduledTaskCategoryID, ScheduledTaskTypeID,
ScheduledTaskCronExpression, ScheduledTaskIsActive, ScheduledTaskNextRunOn
BusinessID, Name, Title,
Details, TaskCategoryID,
CronExpression, ScheduleType, IntervalMinutes,
IsActive, NextRunOn
) VALUES (
:businessID, :name, :title, :details, :categoryID, :typeID, :cron, :isActive, :nextRun
:businessID, :name, :title, :details, :categoryID, :cron, :scheduleType, :intervalMinutes, :isActive, :nextRun
)
", {
businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
@ -192,19 +217,65 @@ try {
title: { value: taskTitle, cfsqltype: "cf_sql_varchar" },
details: { value: taskDetails, cfsqltype: "cf_sql_longvarchar", null: !len(taskDetails) },
categoryID: { value: categoryID, cfsqltype: "cf_sql_integer", null: isNull(categoryID) },
typeID: { value: typeID, cfsqltype: "cf_sql_integer", null: isNull(typeID) },
cron: { value: cronExpression, cfsqltype: "cf_sql_varchar" },
scheduleType: { value: scheduleType, cfsqltype: "cf_sql_varchar" },
intervalMinutes: { value: intervalMinutes, cfsqltype: "cf_sql_integer", null: isNull(intervalMinutes) },
isActive: { value: isActive, cfsqltype: "cf_sql_bit" },
nextRun: { value: nextRunOn, cfsqltype: "cf_sql_timestamp" }
}, { datasource: "payfrit" });
qNew = queryExecute("SELECT LAST_INSERT_ID() as newID", [], { datasource: "payfrit" });
newScheduledTaskID = qNew.newID;
// Create the first task immediately
queryExecute("
INSERT INTO Tasks (
BusinessID, CategoryID, TaskTypeID,
Title, Details, CreatedOn, ClaimedByUserID
) VALUES (
:businessID, :categoryID, :typeID,
:title, :details, NOW(), 0
)
", {
businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
categoryID: { value: categoryID, cfsqltype: "cf_sql_integer", null: isNull(categoryID) },
typeID: { value: 0, cfsqltype: "cf_sql_integer" },
title: { value: taskTitle, cfsqltype: "cf_sql_varchar" },
details: { value: taskDetails, cfsqltype: "cf_sql_longvarchar", null: !len(taskDetails) }
}, { datasource: "payfrit" });
qTask = queryExecute("SELECT LAST_INSERT_ID() as taskID", [], { datasource: "payfrit" });
// Now set the NEXT run time (not the immediate one we just created)
if (scheduleType == "interval" || scheduleType == "interval_after_completion") {
if (scheduleType == "interval_after_completion") {
// After-completion: don't schedule next until task is completed
actualNextRun = javaCast("null", "");
} else {
// Fixed interval: next run = NOW + interval
actualNextRun = dateAdd("n", intervalMinutes, now());
}
} else {
// Cron-based
actualNextRun = calculateNextRun(cronExpression);
}
queryExecute("
UPDATE ScheduledTaskDefinitions
SET LastRunOn = NOW(),
NextRunOn = :nextRun
WHERE ID = :id
", {
nextRun: { value: actualNextRun, cfsqltype: "cf_sql_timestamp", null: isNull(actualNextRun) },
id: { value: newScheduledTaskID, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
apiAbort({
"OK": true,
"SCHEDULED_TASK_ID": qNew.newID,
"NEXT_RUN": dateTimeFormat(nextRunOn, "yyyy-mm-dd HH:nn:ss"),
"MESSAGE": "Scheduled task created"
"SCHEDULED_TASK_ID": newScheduledTaskID,
"TASK_ID": qTask.taskID,
"NEXT_RUN": isNull(actualNextRun) ? "" : dateTimeFormat(actualNextRun, "yyyy-mm-dd HH:nn:ss"),
"MESSAGE": "Scheduled task created and first task added"
});
}

View file

@ -15,27 +15,48 @@ try {
// Create ScheduledTaskDefinitions table
queryExecute("
CREATE TABLE IF NOT EXISTS ScheduledTaskDefinitions (
ScheduledTaskID INT AUTO_INCREMENT PRIMARY KEY,
ScheduledTaskBusinessID INT NOT NULL,
ScheduledTaskName VARCHAR(100) NOT NULL,
ScheduledTaskCategoryID INT NULL,
ScheduledTaskTypeID INT NULL,
ScheduledTaskTitle VARCHAR(255) NOT NULL,
ScheduledTaskDetails TEXT NULL,
ScheduledTaskCronExpression VARCHAR(100) NOT NULL,
ScheduledTaskIsActive BIT(1) DEFAULT b'1',
ScheduledTaskLastRunOn DATETIME NULL,
ScheduledTaskNextRunOn DATETIME NULL,
ScheduledTaskCreatedOn DATETIME DEFAULT CURRENT_TIMESTAMP,
ScheduledTaskCreatedByUserID INT NULL,
INDEX idx_business (ScheduledTaskBusinessID),
INDEX idx_active_next (ScheduledTaskIsActive, ScheduledTaskNextRunOn)
ID INT AUTO_INCREMENT PRIMARY KEY,
BusinessID INT NOT NULL,
Name VARCHAR(100) NOT NULL,
TaskCategoryID INT NULL,
TaskTypeID INT NULL,
Title VARCHAR(255) NOT NULL,
Details TEXT NULL,
CronExpression VARCHAR(100) NOT NULL,
ScheduleType VARCHAR(20) DEFAULT 'cron',
IntervalMinutes INT NULL,
IsActive BIT(1) DEFAULT b'1',
LastRunOn DATETIME NULL,
NextRunOn DATETIME NULL,
CreatedOn DATETIME DEFAULT CURRENT_TIMESTAMP,
CreatedByUserID INT NULL,
INDEX idx_business (BusinessID),
INDEX idx_active_next (IsActive, NextRunOn)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
", [], { datasource: "payfrit" });
// Add new columns if they don't exist (for existing tables)
try {
queryExecute("
ALTER TABLE ScheduledTaskDefinitions
ADD COLUMN ScheduleType VARCHAR(20) DEFAULT 'cron' AFTER CronExpression
", [], { datasource: "payfrit" });
} catch (any e) {
// Column likely already exists
}
try {
queryExecute("
ALTER TABLE ScheduledTaskDefinitions
ADD COLUMN IntervalMinutes INT NULL AFTER ScheduleType
", [], { datasource: "payfrit" });
} catch (any e) {
// Column likely already exists
}
apiAbort({
"OK": true,
"MESSAGE": "ScheduledTaskDefinitions table created/verified"
"MESSAGE": "ScheduledTaskDefinitions table created/verified with interval support"
});
} catch (any e) {

View file

@ -97,11 +97,13 @@ try {
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "ScheduledTaskID is required" });
}
// Verify exists and get cron expression
// Verify exists and get cron expression and schedule type
qCheck = queryExecute("
SELECT ScheduledTaskID, ScheduledTaskCronExpression as CronExpression
SELECT ScheduledTaskID, CronExpression as CronExpression,
COALESCE(ScheduleType, 'cron') as ScheduleType,
IntervalMinutes as IntervalMinutes
FROM ScheduledTaskDefinitions
WHERE ScheduledTaskID = :id AND ScheduledTaskBusinessID = :businessID
WHERE ID = :id AND BusinessID = :businessID
", {
id: { value: taskID, cfsqltype: "cf_sql_integer" },
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
@ -111,20 +113,26 @@ try {
apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Scheduled task not found" });
}
// If enabling, recalculate next run time
// If enabling, recalculate next run time based on schedule type
nextRunUpdate = "";
if (isActive) {
if ((qCheck.ScheduleType == "interval" || qCheck.ScheduleType == "interval_after_completion") && !isNull(qCheck.IntervalMinutes) && qCheck.IntervalMinutes > 0) {
// Interval-based: next run = NOW + interval minutes
nextRunOn = dateAdd("n", qCheck.IntervalMinutes, now());
} else {
// Cron-based: use cron parser
nextRunOn = calculateNextRun(qCheck.CronExpression);
nextRunUpdate = ", ScheduledTaskNextRunOn = :nextRun";
}
nextRunUpdate = ", NextRunOn = :nextRun";
}
// Update status
if (isActive) {
queryExecute("
UPDATE ScheduledTaskDefinitions SET
ScheduledTaskIsActive = :isActive,
ScheduledTaskNextRunOn = :nextRun
WHERE ScheduledTaskID = :id
IsActive = :isActive,
NextRunOn = :nextRun
WHERE ID = :id
", {
isActive: { value: isActive, cfsqltype: "cf_sql_bit" },
nextRun: { value: nextRunOn, cfsqltype: "cf_sql_timestamp" },
@ -132,8 +140,8 @@ try {
}, { datasource: "payfrit" });
} else {
queryExecute("
UPDATE ScheduledTaskDefinitions SET ScheduledTaskIsActive = :isActive
WHERE ScheduledTaskID = :id
UPDATE ScheduledTaskDefinitions SET IsActive = :isActive
WHERE ID = :id
", {
isActive: { value: isActive, cfsqltype: "cf_sql_bit" },
id: { value: taskID, cfsqltype: "cf_sql_integer" }

View file

@ -21,8 +21,8 @@ if (businessId <= 0 || userId <= 0) {
try {
// Update employee record
queryExecute("
UPDATE lt_Users_Businesses_Employees
SET EmployeeIsActive = ?
UPDATE Employees
SET IsActive = ?
WHERE BusinessID = ? AND UserID = ?
", [
{ value: isActive, cfsqltype: "cf_sql_bit" },
@ -32,12 +32,12 @@ try {
// Get updated record
q = queryExecute("
SELECT e.EmployeeID, e.BusinessID, e.UserID, e.EmployeeStatusID,
CAST(e.EmployeeIsActive AS UNSIGNED) AS EmployeeIsActive,
b.BusinessName, u.UserFirstName, u.UserLastName
FROM lt_Users_Businesses_Employees e
JOIN Businesses b ON e.BusinessID = b.BusinessID
JOIN Users u ON e.UserID = u.UserID
SELECT e.ID, e.BusinessID, e.UserID, e.StatusID,
CAST(e.IsActive AS UNSIGNED) AS IsActive,
b.Name, u.FirstName, u.LastName
FROM Employees e
JOIN Businesses b ON e.BusinessID = b.ID
JOIN Users u ON e.UserID = u.ID
WHERE e.BusinessID = ? AND e.UserID = ?
", [
{ value: businessId, cfsqltype: "cf_sql_integer" },
@ -49,13 +49,13 @@ try {
"OK": true,
"MESSAGE": "Employee updated",
"EMPLOYEE": {
"EmployeeID": q.EmployeeID,
"EmployeeID": q.ID,
"BusinessID": q.BusinessID,
"BusinessName": q.BusinessName,
"Name": q.Name,
"UserID": q.UserID,
"UserName": trim(q.UserFirstName & " " & q.UserLastName),
"StatusID": q.EmployeeStatusID,
"IsActive": q.EmployeeIsActive
"UserName": trim(q.FirstName & " " & q.LastName),
"StatusID": q.StatusID,
"IsActive": q.IsActive
}
}));
} else {

View file

@ -7,11 +7,11 @@ response = { "OK": false };
try {
queryExecute("
UPDATE Businesses SET BusinessHeaderImageExtension = 'jpg' WHERE BusinessID = 37
UPDATE Businesses SET HeaderImageExtension = 'jpg' WHERE ID = 37
", {}, { datasource: "payfrit" });
response.OK = true;
response.message = "Set BusinessHeaderImageExtension to 'jpg' for business 37";
response.message = "Set HeaderImageExtension to 'jpg' for business 37";
} catch (any e) {
response.error = e.message;
}

View file

@ -5,7 +5,7 @@
<cfscript>
/**
* Setup Lazy Daisy Beacons
* Creates a beacon for each service point and links them
* Creates a beacon for each service point and assigns them
*/
response = { "OK": false, "steps": [] };
@ -14,10 +14,10 @@ try {
// Get all service points for Lazy Daisy
qServicePoints = queryExecute("
SELECT ServicePointID, ServicePointName
SELECT ID, Name
FROM ServicePoints
WHERE ServicePointBusinessID = :bizID AND ServicePointIsActive = 1
ORDER BY ServicePointID
WHERE BusinessID = :bizID AND IsActive = 1
ORDER BY SortOrder, ID
", { bizID: lazyDaisyID }, { datasource: "payfrit" });
response.steps.append("Found " & qServicePoints.recordCount & " service points for Lazy Daisy");
@ -25,20 +25,20 @@ try {
// Create a beacon for each service point
beaconsCreated = 0;
for (sp in qServicePoints) {
beaconName = "Beacon - " & sp.ServicePointName;
beaconName = "Beacon - " & sp.Name;
// Check if beacon already exists for this business with this name
qExisting = queryExecute("
SELECT BeaconID FROM Beacons
WHERE BeaconBusinessID = :bizId AND BeaconName = :name
SELECT ID FROM Beacons
WHERE BusinessID = :bizId AND Name = :name
", { bizId: lazyDaisyID, name: beaconName }, { datasource: "payfrit" });
if (qExisting.recordCount == 0) {
// Generate a unique UUID for this beacon (32 hex chars, no dashes)
beaconUUID = "PAYFRIT00037" & numberFormat(sp.ServicePointID, "0000000000000000000");
beaconUUID = "PAYFRIT00037" & numberFormat(sp.ID, "0000000000000000000");
queryExecute("
INSERT INTO Beacons (BeaconBusinessID, BeaconName, BeaconUUID, BeaconIsActive)
INSERT INTO Beacons (BusinessID, Name, UUID, IsActive)
VALUES (:bizId, :name, :uuid, 1)
", {
bizId: lazyDaisyID,
@ -49,18 +49,18 @@ try {
qNewBeacon = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
newBeaconId = qNewBeacon.id;
// Create assignment to service point
// Assign beacon directly to service point
queryExecute("
INSERT INTO lt_Beacon_Businesses_ServicePoints
(BeaconID, BusinessID, ServicePointID, lt_Beacon_Businesses_ServicePointAssignedByUserID)
VALUES (:beaconId, :bizId, :spId, 1)
UPDATE ServicePoints
SET BeaconID = :beaconId, AssignedByUserID = 1
WHERE ID = :spId AND BusinessID = :bizId
", {
beaconId: newBeaconId,
bizId: lazyDaisyID,
spId: sp.ServicePointID
spId: sp.ID
}, { datasource: "payfrit" });
response.steps.append("Created beacon '" & beaconName & "' (ID: " & newBeaconId & ") -> " & sp.ServicePointName);
response.steps.append("Created beacon '" & beaconName & "' (ID: " & newBeaconId & ") -> " & sp.Name);
beaconsCreated++;
} else {
response.steps.append("Beacon '" & beaconName & "' already exists, skipping");
@ -69,13 +69,14 @@ try {
// Get final status
qFinal = queryExecute("
SELECT lt.BeaconID, b.BeaconUUID, b.BeaconName, lt.BusinessID, biz.BusinessName, lt.ServicePointID, sp.ServicePointName
FROM lt_Beacon_Businesses_ServicePoints lt
JOIN Beacons b ON b.BeaconID = lt.BeaconID
JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
WHERE lt.BusinessID = :bizId
ORDER BY sp.ServicePointName
SELECT sp.ID AS ServicePointID, sp.BeaconID, sp.BusinessID,
b.Name AS BeaconName, b.UUID, sp.Name AS ServicePointName,
biz.Name AS BusinessName
FROM ServicePoints sp
JOIN Beacons b ON b.ID = sp.BeaconID
JOIN Businesses biz ON biz.ID = sp.BusinessID
WHERE sp.BusinessID = :bizId AND sp.BeaconID IS NOT NULL
ORDER BY sp.Name
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
beacons = [];
@ -83,8 +84,9 @@ try {
arrayAppend(beacons, {
"BeaconID": qFinal.BeaconID[i],
"BeaconName": qFinal.BeaconName[i],
"UUID": qFinal.BeaconUUID[i],
"UUID": qFinal.UUID[i],
"BusinessName": qFinal.BusinessName[i],
"ServicePointID": qFinal.ServicePointID[i],
"ServicePointName": qFinal.ServicePointName[i]
});
}

View file

@ -13,19 +13,19 @@ try {
// Hours: Mon-Thu: 11am-10pm, Fri-Sat: 11am-11pm, Sun: 11am-10pm
// Get California StateID
qState = queryExecute("SELECT tt_StateID FROM tt_States WHERE tt_StateAbbreviation = 'CA' LIMIT 1");
qState = queryExecute("SELECT tt_StateID FROM tt_States WHERE Abbreviation = 'CA' LIMIT 1");
stateId = qState.recordCount > 0 ? qState.tt_StateID : 5; // Default to 5 if not found
// Check if Big Dean's already has an address
existingAddr = queryExecute("
SELECT AddressID FROM Addresses
WHERE AddressBusinessID = :bizId AND AddressUserID = 0
SELECT ID FROM Addresses
WHERE BusinessID = :bizId AND UserID = 0
", { bizId: businessId });
if (existingAddr.recordCount == 0) {
// Insert new address
queryExecute("
INSERT INTO Addresses (AddressUserID, AddressBusinessID, AddressTypeID, AddressLine1, AddressCity, AddressStateID, AddressZIPCode, AddressIsDeleted, AddressAddedOn)
INSERT INTO Addresses (UserID, BusinessID, AddressTypeID, Line1, City, StateID, ZIPCode, IsDeleted, AddedOn)
VALUES (0, :bizId, '2', :line1, :city, :stateId, :zip, 0, NOW())
", {
bizId: businessId,
@ -39,8 +39,8 @@ try {
// Update existing address
queryExecute("
UPDATE Addresses
SET AddressLine1 = :line1, AddressCity = :city, AddressStateID = :stateId, AddressZIPCode = :zip
WHERE AddressBusinessID = :bizId AND AddressUserID = 0
SET Line1 = :line1, City = :city, StateID = :stateId, ZIPCode = :zip
WHERE BusinessID = :bizId AND UserID = 0
", {
bizId: businessId,
line1: "1615 Ocean Front Walk",
@ -53,7 +53,7 @@ try {
// Check existing hours for this business
existingHours = queryExecute("
SELECT COUNT(*) as cnt FROM Hours WHERE HoursBusinessID = :bizId
SELECT COUNT(*) as cnt FROM Hours WHERE BusinessID = :bizId
", { bizId: businessId });
if (existingHours.cnt == 0) {
@ -64,39 +64,39 @@ try {
// Sun: 11am-10pm (day 1)
// Sunday (1): 11am-10pm
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 1, '11:00:00', '22:00:00')", { bizId: businessId });
queryExecute("INSERT INTO Hours (BusinessID, DayID, OpenTime, ClosingTime) VALUES (:bizId, 1, '11:00:00', '22:00:00')", { bizId: businessId });
// Monday (2): 11am-10pm
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 2, '11:00:00', '22:00:00')", { bizId: businessId });
queryExecute("INSERT INTO Hours (BusinessID, DayID, OpenTime, ClosingTime) VALUES (:bizId, 2, '11:00:00', '22:00:00')", { bizId: businessId });
// Tuesday (3): 11am-10pm
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 3, '11:00:00', '22:00:00')", { bizId: businessId });
queryExecute("INSERT INTO Hours (BusinessID, DayID, OpenTime, ClosingTime) VALUES (:bizId, 3, '11:00:00', '22:00:00')", { bizId: businessId });
// Wednesday (4): 11am-10pm
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 4, '11:00:00', '22:00:00')", { bizId: businessId });
queryExecute("INSERT INTO Hours (BusinessID, DayID, OpenTime, ClosingTime) VALUES (:bizId, 4, '11:00:00', '22:00:00')", { bizId: businessId });
// Thursday (5): 11am-10pm
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 5, '11:00:00', '22:00:00')", { bizId: businessId });
queryExecute("INSERT INTO Hours (BusinessID, DayID, OpenTime, ClosingTime) VALUES (:bizId, 5, '11:00:00', '22:00:00')", { bizId: businessId });
// Friday (6): 11am-11pm
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 6, '11:00:00', '23:00:00')", { bizId: businessId });
queryExecute("INSERT INTO Hours (BusinessID, DayID, OpenTime, ClosingTime) VALUES (:bizId, 6, '11:00:00', '23:00:00')", { bizId: businessId });
// Saturday (7): 11am-11pm
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 7, '11:00:00', '23:00:00')", { bizId: businessId });
queryExecute("INSERT INTO Hours (BusinessID, DayID, OpenTime, ClosingTime) VALUES (:bizId, 7, '11:00:00', '23:00:00')", { bizId: businessId });
response["HOURS_ACTION"] = "inserted 7 days";
} else {
// Update existing hours
// Mon-Thu: 11am-10pm
queryExecute("UPDATE Hours SET HoursOpenTime = '11:00:00', HoursClosingTime = '22:00:00' WHERE HoursBusinessID = :bizId AND HoursDayID IN (1, 2, 3, 4, 5)", { bizId: businessId });
queryExecute("UPDATE Hours SET OpenTime = '11:00:00', ClosingTime = '22:00:00' WHERE BusinessID = :bizId AND DayID IN (1, 2, 3, 4, 5)", { bizId: businessId });
// Fri-Sat: 11am-11pm
queryExecute("UPDATE Hours SET HoursOpenTime = '11:00:00', HoursClosingTime = '23:00:00' WHERE HoursBusinessID = :bizId AND HoursDayID IN (6, 7)", { bizId: businessId });
queryExecute("UPDATE Hours SET OpenTime = '11:00:00', ClosingTime = '23:00:00' WHERE BusinessID = :bizId AND DayID IN (6, 7)", { bizId: businessId });
response["HOURS_ACTION"] = "updated";
}
// Update phone on Businesses table (if column exists)
try {
queryExecute("UPDATE Businesses SET BusinessPhone = :phone WHERE BusinessID = :bizId", {
queryExecute("UPDATE Businesses SET Phone = :phone WHERE ID = :bizId", {
phone: "(310) 393-2666",
bizId: businessId
});
@ -107,35 +107,35 @@ try {
// Verify the data
address = queryExecute("
SELECT a.*, s.tt_StateAbbreviation
SELECT a.*, s.Abbreviation
FROM Addresses a
LEFT JOIN tt_States s ON s.tt_StateID = a.AddressStateID
WHERE a.AddressBusinessID = :bizId AND a.AddressUserID = 0
LEFT JOIN tt_States s ON s.ID = a.StateID
WHERE a.BusinessID = :bizId AND a.UserID = 0
", { bizId: businessId });
hours = queryExecute("
SELECT h.*, d.tt_DayName
FROM Hours h
JOIN tt_Days d ON d.tt_DayID = h.HoursDayID
WHERE h.HoursBusinessID = :bizId
ORDER BY h.HoursDayID
JOIN tt_Days d ON d.ID = h.DayID
WHERE h.BusinessID = :bizId
ORDER BY h.DayID
", { bizId: businessId });
response["OK"] = true;
response["BUSINESS_ID"] = businessId;
response["ADDRESS"] = address.recordCount > 0 ? {
"line1": address.AddressLine1,
"city": address.AddressCity,
"state": address.tt_StateAbbreviation,
"zip": address.AddressZIPCode
"line1": address.Line1,
"city": address.City,
"state": address.Abbreviation,
"zip": address.ZIPCode
} : "not found";
hoursArr = [];
for (h in hours) {
arrayAppend(hoursArr, {
"day": h.tt_DayName,
"open": timeFormat(h.HoursOpenTime, "h:mm tt"),
"close": timeFormat(h.HoursClosingTime, "h:mm tt")
"open": timeFormat(h.OpenTime, "h:mm tt"),
"close": timeFormat(h.ClosingTime, "h:mm tt")
});
}
response["HOURS"] = hoursArr;

View file

@ -9,19 +9,19 @@ response = { "OK": false };
try {
// Check if Big Dean's already has stations
existing = queryExecute("
SELECT COUNT(*) as cnt FROM Stations WHERE StationBusinessID = :bizId
SELECT COUNT(*) as cnt FROM Stations WHERE BusinessID = :bizId
", { bizId: businessId });
if (existing.cnt == 0) {
// Insert Kitchen station
queryExecute("
INSERT INTO Stations (StationBusinessID, StationName, StationColor, StationSortOrder, StationIsActive)
INSERT INTO Stations (BusinessID, Name, Color, SortOrder, IsActive)
VALUES (:bizId, 'Kitchen', :color1, 1, 1)
", { bizId: businessId, color1: "##FF5722" });
// Insert Bar station
queryExecute("
INSERT INTO Stations (StationBusinessID, StationName, StationColor, StationSortOrder, StationIsActive)
INSERT INTO Stations (BusinessID, Name, Color, SortOrder, IsActive)
VALUES (:bizId, 'Bar', :color2, 2, 1)
", { bizId: businessId, color2: "##2196F3" });
@ -32,18 +32,18 @@ try {
// Get current stations
stations = queryExecute("
SELECT StationID, StationName, StationColor, StationSortOrder
SELECT ID, Name, Color, SortOrder
FROM Stations
WHERE StationBusinessID = :bizId AND StationIsActive = 1
ORDER BY StationSortOrder
WHERE BusinessID = :bizId AND IsActive = 1
ORDER BY SortOrder
", { bizId: businessId });
stationArr = [];
for (s in stations) {
arrayAppend(stationArr, {
"StationID": s.StationID,
"StationName": s.StationName,
"StationColor": s.StationColor
"StationID": s.ID,
"Name": s.Name,
"Color": s.Color
});
}

View file

@ -9,67 +9,62 @@ try {
lazyDaisyID = 37;
// Get all beacons
qBeacons = queryExecute("SELECT BeaconID, BeaconUUID FROM Beacons", {}, { datasource: "payfrit" });
qBeacons = queryExecute("SELECT ID, UUID FROM Beacons", {}, { datasource: "payfrit" });
response.steps.append("Found " & qBeacons.recordCount & " beacons");
// Create service point for Table 1 if it doesn't exist
qSP = queryExecute("
SELECT ServicePointID FROM ServicePoints
WHERE ServicePointBusinessID = :bizID AND ServicePointName = 'Table 1'
SELECT ID FROM ServicePoints
WHERE BusinessID = :bizID AND Name = 'Table 1'
", { bizID: lazyDaisyID }, { datasource: "payfrit" });
if (qSP.recordCount == 0) {
queryExecute("
INSERT INTO ServicePoints (ServicePointBusinessID, ServicePointName, ServicePointTypeID)
VALUES (:bizID, 'Table 1', 1)
INSERT INTO ServicePoints (BusinessID, Name)
VALUES (:bizID, 'Table 1')
", { bizID: lazyDaisyID }, { datasource: "payfrit" });
qSP = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
servicePointID = qSP.id;
response.steps.append("Created service point 'Table 1' (ID: " & servicePointID & ")");
} else {
servicePointID = qSP.ServicePointID;
servicePointID = qSP.ID;
response.steps.append("Found existing service point 'Table 1' (ID: " & servicePointID & ")");
}
// Map all beacons to Lazy Daisy with Table 1
// Assign all beacons to the Table 1 service point
for (i = 1; i <= qBeacons.recordCount; i++) {
beaconID = qBeacons.BeaconID[i];
beaconID = qBeacons.ID[i];
// Check if mapping exists
qMap = queryExecute("
SELECT * FROM lt_Beacon_Businesses_ServicePoints WHERE BeaconID = :beaconID
// Unassign this beacon from any other service point first
queryExecute("
UPDATE ServicePoints SET BeaconID = NULL, AssignedByUserID = NULL
WHERE BeaconID = :beaconID
", { beaconID: beaconID }, { datasource: "payfrit" });
if (qMap.recordCount == 0) {
// Assign beacon to Table 1 service point
queryExecute("
INSERT INTO lt_Beacon_Businesses_ServicePoints (BeaconID, BusinessID, ServicePointID)
VALUES (:beaconID, :bizID, :spID)
UPDATE ServicePoints SET BeaconID = :beaconID, AssignedByUserID = 1
WHERE ID = :spID AND BusinessID = :bizID
", { beaconID: beaconID, bizID: lazyDaisyID, spID: servicePointID }, { datasource: "payfrit" });
response.steps.append("Created mapping for beacon " & beaconID);
} else {
queryExecute("
UPDATE lt_Beacon_Businesses_ServicePoints
SET BusinessID = :bizID, ServicePointID = :spID
WHERE BeaconID = :beaconID
", { beaconID: beaconID, bizID: lazyDaisyID, spID: servicePointID }, { datasource: "payfrit" });
response.steps.append("Updated mapping for beacon " & beaconID);
}
response.steps.append("Assigned beacon " & beaconID & " to Table 1");
}
// Get final status
qFinal = queryExecute("
SELECT lt.BeaconID, b.BeaconUUID, lt.BusinessID, biz.BusinessName, lt.ServicePointID, sp.ServicePointName
FROM lt_Beacon_Businesses_ServicePoints lt
JOIN Beacons b ON b.BeaconID = lt.BeaconID
JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
SELECT sp.ID AS ServicePointID, sp.BeaconID, sp.BusinessID,
b.Name AS BeaconName, b.UUID, sp.Name AS ServicePointName,
biz.Name AS BusinessName
FROM ServicePoints sp
JOIN Beacons b ON b.ID = sp.BeaconID
JOIN Businesses biz ON biz.ID = sp.BusinessID
WHERE sp.BeaconID IS NOT NULL
", {}, { datasource: "payfrit" });
beacons = [];
for (i = 1; i <= qFinal.recordCount; i++) {
arrayAppend(beacons, {
"BeaconID": qFinal.BeaconID[i],
"UUID": qFinal.BeaconUUID[i],
"UUID": qFinal.UUID[i],
"BusinessID": qFinal.BusinessID[i],
"BusinessName": qFinal.BusinessName[i],
"ServicePointID": qFinal.ServicePointID[i],

View file

@ -11,32 +11,32 @@
<cfscript>
/**
* Setup Modifier Templates system
* 1. Add ItemIsModifierTemplate column to Items
* 2. Create ItemTemplateLinks table
* 1. Add IsModifierTemplate column to Items
* 2. Create lt_ItemID_TemplateItemID table
*/
response = { "OK": false, "steps": [] };
try {
// Step 1: Add ItemIsModifierTemplate column if it doesn't exist
// Step 1: Add IsModifierTemplate column if it doesn't exist
try {
queryExecute("
ALTER TABLE Items ADD COLUMN ItemIsModifierTemplate TINYINT(1) DEFAULT 0
ALTER TABLE Items ADD COLUMN IsModifierTemplate TINYINT(1) DEFAULT 0
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Added ItemIsModifierTemplate column");
arrayAppend(response.steps, "Added IsModifierTemplate column");
} catch (any e) {
if (findNoCase("Duplicate column", e.message)) {
arrayAppend(response.steps, "ItemIsModifierTemplate column already exists");
arrayAppend(response.steps, "IsModifierTemplate column already exists");
} else {
arrayAppend(response.steps, "Error adding column: " & e.message);
}
}
// Step 2: Create ItemTemplateLinks table if it doesn't exist
// Step 2: Create lt_ItemID_TemplateItemID table if it doesn't exist
try {
queryExecute("
CREATE TABLE IF NOT EXISTS ItemTemplateLinks (
LinkID INT AUTO_INCREMENT PRIMARY KEY,
CREATE TABLE IF NOT EXISTS lt_ItemID_TemplateItemID (
ID INT AUTO_INCREMENT PRIMARY KEY,
ItemID INT NOT NULL,
TemplateItemID INT NOT NULL,
SortOrder INT DEFAULT 0,
@ -45,9 +45,9 @@ try {
INDEX idx_template (TemplateItemID)
)
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Created ItemTemplateLinks table");
arrayAppend(response.steps, "Created lt_ItemID_TemplateItemID table");
} catch (any e) {
arrayAppend(response.steps, "ItemTemplateLinks: " & e.message);
arrayAppend(response.steps, "lt_ItemID_TemplateItemID: " & e.message);
}
response["OK"] = true;

View file

@ -13,20 +13,20 @@
<cfset queryExecute("
CREATE TABLE IF NOT EXISTS Stations (
StationID INT AUTO_INCREMENT PRIMARY KEY,
StationBusinessID INT NOT NULL,
StationName VARCHAR(100) NOT NULL,
StationColor VARCHAR(7) DEFAULT '##666666',
StationSortOrder INT DEFAULT 0,
StationIsActive TINYINT(1) DEFAULT 1,
StationAddedOn DATETIME DEFAULT NOW(),
FOREIGN KEY (StationBusinessID) REFERENCES Businesses(BusinessID)
BusinessID INT NOT NULL,
Name VARCHAR(100) NOT NULL,
Color VARCHAR(7) DEFAULT '##666666',
SortOrder INT DEFAULT 0,
IsActive TINYINT(1) DEFAULT 1,
AddedOn DATETIME DEFAULT NOW(),
FOREIGN KEY (BusinessID) REFERENCES Businesses(BusinessID)
)
", [], { datasource = "payfrit" })>
<!--- Add ItemStationID column to Items table if it doesn't exist --->
<!--- Add StationID column to Items table if it doesn't exist --->
<cftry>
<cfset queryExecute("
ALTER TABLE Items ADD COLUMN ItemStationID INT DEFAULT NULL
ALTER TABLE Items ADD COLUMN StationID INT DEFAULT NULL
", [], { datasource = "payfrit" })>
<cfset stationColumnAdded = true>
<cfcatch>
@ -39,7 +39,7 @@
<cfif stationColumnAdded>
<cftry>
<cfset queryExecute("
ALTER TABLE Items ADD FOREIGN KEY (ItemStationID) REFERENCES Stations(StationID)
ALTER TABLE Items ADD FOREIGN KEY (StationID) REFERENCES Stations(StationID)
", [], { datasource = "payfrit" })>
<cfcatch></cfcatch>
</cftry>
@ -47,12 +47,12 @@
<!--- Create some default stations for business 1 (In and Out Burger) if none exist --->
<cfset qCheck = queryExecute("
SELECT COUNT(*) AS cnt FROM Stations WHERE StationBusinessID = 1
SELECT COUNT(*) AS cnt FROM Stations WHERE BusinessID = 1
", [], { datasource = "payfrit" })>
<cfif qCheck.cnt EQ 0>
<cfset queryExecute("
INSERT INTO Stations (StationBusinessID, StationName, StationColor, StationSortOrder) VALUES
INSERT INTO Stations (BusinessID, Name, Color, SortOrder) VALUES
(1, 'Grill', '##FF5722', 1),
(1, 'Fry', '##FFC107', 2),
(1, 'Drinks', '##2196F3', 3),

View file

@ -3,30 +3,51 @@
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
// Switch all beacons from one business to another
// Switch beacon mapping from one business to another via join table.
// Beacons.BusinessID (owner) is NOT touched.
fromBiz = 17; // In-N-Out
toBiz = 27; // Big Dean's
// Remove mapping for source business
queryExecute("
UPDATE lt_Beacon_Businesses_ServicePoints
SET BusinessID = :toBiz
DELETE FROM lt_BeaconsID_BusinessesID
WHERE BusinessID = :fromBiz
", { fromBiz: fromBiz }, { datasource: "payfrit" });
// Add mapping for target business (for beacons owned by source)
queryExecute("
INSERT INTO lt_BeaconsID_BusinessesID (BeaconID, BusinessID)
SELECT ID, :toBiz FROM Beacons WHERE BusinessID = :fromBiz
ON DUPLICATE KEY UPDATE ID = ID
", { toBiz: toBiz, fromBiz: fromBiz }, { datasource: "payfrit" });
// Clear ServicePoints.BeaconID for source business (no longer valid)
queryExecute("
UPDATE ServicePoints
SET BeaconID = NULL, AssignedByUserID = NULL
WHERE BusinessID = :fromBiz AND BeaconID IS NOT NULL
", { fromBiz: fromBiz }, { datasource: "payfrit" });
// Get current state
q = queryExecute("
SELECT lt.*, b.BusinessName
FROM lt_Beacon_Businesses_ServicePoints lt
JOIN Businesses b ON b.BusinessID = lt.BusinessID
SELECT sp.ID AS ServicePointID, sp.BeaconID, sp.BusinessID,
b.Name AS BeaconName, biz.Name AS BusinessName,
sp.Name AS ServicePointName
FROM ServicePoints sp
JOIN Beacons b ON b.ID = sp.BeaconID
JOIN Businesses biz ON biz.ID = sp.BusinessID
WHERE sp.BeaconID IS NOT NULL
", {}, { datasource: "payfrit" });
rows = [];
for (row in q) {
arrayAppend(rows, {
"BeaconID": row.BeaconID,
"BeaconName": row.BeaconName,
"BusinessID": row.BusinessID,
"BusinessName": row.BusinessName,
"ServicePointID": row.ServicePointID
"ServicePointID": row.ServicePointID,
"ServicePointName": row.ServicePointName
});
}

View file

@ -7,10 +7,10 @@
<cfset queryExecute(
"
INSERT INTO Tasks (
TaskBusinessID,
TaskOrderID,
TaskClaimedByUserID,
TaskAddedOn
BusinessID,
OrderID,
ClaimedByUserID,
CreatedOn
) VALUES (
1,
999,

View file

@ -2,16 +2,16 @@
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
qTask = queryExecute("
SELECT TaskID, TaskTypeID, TaskClaimedByUserID, TaskCompletedOn
SELECT ID, TaskTypeID, ClaimedByUserID, CompletedOn
FROM Tasks
WHERE TaskID = 57
WHERE ID = 57
", [], { datasource: "payfrit" });
writeOutput(serializeJSON({
"OK": true,
"TaskID": qTask.TaskID,
"TaskID": qTask.ID,
"TaskTypeID": qTask.TaskTypeID,
"TaskClaimedByUserID": qTask.TaskClaimedByUserID,
"TaskCompletedOn": qTask.TaskCompletedOn
"ClaimedByUserID": qTask.ClaimedByUserID,
"CompletedOn": qTask.CompletedOn
}));
</cfscript>

View file

@ -3,42 +3,57 @@
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
// Update Beacon 2 to point to In-N-Out (BusinessID 17)
// Update beacon mapping via join table. Owner (Beacons.BusinessID) is NOT changed.
beaconId = 2;
oldBusinessId = 37; // previous mapping
newBusinessId = 17;
// Remove old mapping
queryExecute("
UPDATE lt_Beacon_Businesses_ServicePoints
SET BusinessID = :newBizId
WHERE BeaconID = :beaconId
", { newBizId: newBusinessId, beaconId: beaconId }, { datasource: "payfrit" });
DELETE FROM lt_BeaconsID_BusinessesID
WHERE BeaconID = :beaconId AND BusinessID = :oldBizId
", { beaconId: beaconId, oldBizId: oldBusinessId }, { datasource: "payfrit" });
// Add new mapping
queryExecute("
INSERT INTO lt_BeaconsID_BusinessesID (BeaconID, BusinessID)
VALUES (:beaconId, :newBizId)
ON DUPLICATE KEY UPDATE ID = ID
", { beaconId: beaconId, newBizId: newBusinessId }, { datasource: "payfrit" });
// Clear ServicePoints.BeaconID for old business where this beacon was assigned
queryExecute("
UPDATE ServicePoints
SET BeaconID = NULL, AssignedByUserID = NULL
WHERE BeaconID = :beaconId AND BusinessID = :oldBizId
", { beaconId: beaconId, oldBizId: oldBusinessId }, { datasource: "payfrit" });
// Get current state
q = queryExecute("
SELECT
b.BeaconID,
b.BeaconUUID,
b.BeaconName,
lt.BusinessID,
lt.ServicePointID,
biz.BusinessName,
sp.ServicePointName
b.ID AS BeaconID,
b.UUID,
b.Name AS BeaconName,
b.BusinessID AS BeaconBusinessID,
sp.ID AS ServicePointID,
sp.Name AS ServicePointName,
sp.BusinessID AS ServicePointBusinessID,
biz.Name AS BusinessName
FROM Beacons b
LEFT JOIN lt_Beacon_Businesses_ServicePoints lt ON lt.BeaconID = b.BeaconID
LEFT JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
WHERE b.BeaconIsActive = 1
ORDER BY b.BeaconID
LEFT JOIN ServicePoints sp ON sp.BeaconID = b.ID
LEFT JOIN Businesses biz ON biz.ID = b.BusinessID
WHERE b.IsActive = 1
ORDER BY b.ID
", {}, { datasource: "payfrit" });
rows = [];
for (row in q) {
arrayAppend(rows, {
"BeaconID": row.BeaconID,
"BeaconUUID": row.BeaconUUID,
"BeaconName": row.BeaconName ?: "",
"BusinessID": row.BusinessID ?: 0,
"BusinessName": row.BusinessName ?: "",
"UUID": row.UUID,
"BeaconName": row.BeaconName,
"BeaconBusinessID": row.BeaconBusinessID,
"BusinessName": row.BusinessName,
"ServicePointID": row.ServicePointID ?: 0,
"ServicePointName": row.ServicePointName ?: ""
});

View file

@ -6,72 +6,68 @@
// Update Big Dean's business info
businessId = 27;
// Big Dean's actual address and hours
address = "1615 Ocean Front Walk, Santa Monica, CA 90401";
// Big Dean's actual info
phone = "(310) 393-2666";
hours = "Mon-Thu: 11am-10pm, Fri-Sat: 11am-11pm, Sun: 11am-10pm";
try {
// First get column names from INFORMATION_SCHEMA
cols = queryExecute("
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'payfrit' AND TABLE_NAME = 'Businesses'
ORDER BY ORDINAL_POSITION
");
colNames = [];
for (c in cols) {
arrayAppend(colNames, c.COLUMN_NAME);
}
// Check if we have the columns we need
hasAddress = arrayFindNoCase(colNames, "BusinessAddress") > 0;
hasPhone = arrayFindNoCase(colNames, "BusinessPhone") > 0;
hasHours = arrayFindNoCase(colNames, "BusinessHours") > 0;
// Add columns if missing
if (!hasAddress) {
queryExecute("ALTER TABLE Businesses ADD COLUMN BusinessAddress VARCHAR(255)");
}
if (!hasPhone) {
queryExecute("ALTER TABLE Businesses ADD COLUMN BusinessPhone VARCHAR(50)");
}
if (!hasHours) {
queryExecute("ALTER TABLE Businesses ADD COLUMN BusinessHours VARCHAR(255)");
}
// Update with new info
// Update phone and hours on Businesses table
queryExecute("
UPDATE Businesses
SET BusinessAddress = :address,
BusinessPhone = :phone,
BusinessHours = :hours
WHERE BusinessID = :bizId
SET Phone = :phone,
Hours = :hours
WHERE ID = :bizId
", {
address: address,
phone: phone,
hours: hours,
bizId: businessId
});
}, { datasource: "payfrit" });
// Update or insert address in Addresses table
qAddr = queryExecute("
SELECT ID FROM Addresses
WHERE BusinessID = :bizId AND IsDeleted = 0
LIMIT 1
", { bizId: businessId }, { datasource: "payfrit" });
if (qAddr.recordCount > 0) {
queryExecute("
UPDATE Addresses
SET Line1 = :line1, City = :city, ZIPCode = :zip
WHERE ID = :addrId
", {
line1: "1615 Ocean Front Walk",
city: "Santa Monica",
zip: "90401",
addrId: qAddr.ID
}, { datasource: "payfrit" });
} else {
queryExecute("
INSERT INTO Addresses (BusinessID, UserID, AddressTypeID, Line1, City, ZIPCode, AddedOn)
VALUES (:bizId, 0, 'business', :line1, :city, :zip, NOW())
", {
bizId: businessId,
line1: "1615 Ocean Front Walk",
city: "Santa Monica",
zip: "90401"
}, { datasource: "payfrit" });
}
// Get updated record
updated = queryExecute("
SELECT BusinessID, BusinessName, BusinessAddress, BusinessPhone, BusinessHours
SELECT ID, Name, Phone, Hours
FROM Businesses
WHERE BusinessID = :bizId
", { bizId: businessId });
WHERE ID = :bizId
", { bizId: businessId }, { datasource: "payfrit" });
writeOutput(serializeJSON({
"OK": true,
"MESSAGE": "Updated Big Dean's info",
"COLUMNS_EXISTED": { "address": hasAddress, "phone": hasPhone, "hours": hasHours },
"BUSINESS": {
"BusinessID": updated.BusinessID,
"BusinessName": updated.BusinessName,
"BusinessAddress": updated.BusinessAddress,
"BusinessPhone": updated.BusinessPhone,
"BusinessHours": updated.BusinessHours
"BusinessID": updated.ID,
"Name": updated.Name,
"Phone": updated.Phone,
"Hours": updated.Hours
}
}));

67
api/app/about.cfm Normal file
View file

@ -0,0 +1,67 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8">
<!---
About Screen Content API
Returns content for the mobile app's About screen.
Edit this file to update the app's about information without releasing a new app version.
--->
<cfscript>
try {
// Features displayed on the About screen
// icon: Flutter icon name (see AboutFeature._iconMap in about_info.dart)
features = [
{
"ICON": "qr_code_scanner",
"TITLE": "Scan & Order",
"DESCRIPTION": "Scan the table beacon to browse the menu and order directly from your phone"
},
{
"ICON": "group",
"TITLE": "Group Orders",
"DESCRIPTION": "Invite friends to join your order and split the bill easily"
},
{
"ICON": "delivery_dining",
"TITLE": "Delivery & Takeaway",
"DESCRIPTION": "Order for delivery or pick up when dining in isn't an option"
},
{
"ICON": "payment",
"TITLE": "Easy Payment",
"DESCRIPTION": "Pay your share securely with just a few taps"
}
];
// Contact links displayed on the About screen
// icon: Flutter icon name (see AboutContact._iconMap in about_info.dart)
contacts = [
{
"ICON": "help_outline",
"LABEL": "help.payfrit.com",
"URL": "https://help.payfrit.com"
},
{
"ICON": "language",
"LABEL": "www.payfrit.com",
"URL": "https://www.payfrit.com"
}
];
writeOutput(serializeJSON({
"OK": true,
"DESCRIPTION": "Payfrit makes dining out easier. Order from your table, split the bill with friends, and pay without waiting.",
"FEATURES": features,
"CONTACTS": contacts,
"COPYRIGHT": "© #year(now())# Payfrit. All rights reserved."
}));
} catch (any e) {
writeOutput(serializeJSON({
"OK": false,
"ERROR": "server_error",
"MESSAGE": e.message
}));
}
</cfscript>

View file

@ -34,28 +34,20 @@ if (!structKeyExists(request,"BusinessID") || !isNumeric(request.BusinessID) ||
/* ---------- INPUT ---------- */
data = readJsonBody();
if (
!structKeyExists(data,"lt_Beacon_Businesses_ServicePointID")
|| !isNumeric(data.lt_Beacon_Businesses_ServicePointID)
|| int(data.lt_Beacon_Businesses_ServicePointID) LTE 0
){
apiAbort({OK=false,ERROR="missing_lt_Beacon_Businesses_ServicePointID"});
if (!structKeyExists(data,"ServicePointID") || !isNumeric(data.ServicePointID) || int(data.ServicePointID) LTE 0){
apiAbort({OK=false,ERROR="missing_ServicePointID"});
}
RelID = int(data.lt_Beacon_Businesses_ServicePointID);
ServicePointID = int(data.ServicePointID);
</cfscript>
<!--- Confirm the row exists for this BusinessID (and capture what it was) --->
<!--- Confirm the service point exists for this business and has a beacon assigned --->
<cfquery name="qFind" datasource="payfrit">
SELECT
lt_Beacon_Businesses_ServicePointID,
BeaconID,
ServicePointID
FROM lt_Beacon_Businesses_ServicePoints
WHERE lt_Beacon_Businesses_ServicePointID =
<cfqueryparam cfsqltype="cf_sql_integer" value="#RelID#">
AND BusinessID =
<cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
SELECT ID, BeaconID
FROM ServicePoints
WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#ServicePointID#">
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
AND BeaconID IS NOT NULL
LIMIT 1
</cfquery>
@ -63,28 +55,28 @@ RelID = int(data.lt_Beacon_Businesses_ServicePointID);
<cfoutput>#serializeJSON({
"OK"=false,
"ERROR"="not_found",
"lt_Beacon_Businesses_ServicePointID"=RelID,
"ServicePointID"=ServicePointID,
"BusinessID"=(request.BusinessID & "")
})#</cfoutput>
<cfabort>
</cfif>
<!--- Delete it --->
<cfset removedBeaconID = qFind.BeaconID>
<!--- Unassign beacon from service point --->
<cfquery datasource="payfrit">
DELETE FROM lt_Beacon_Businesses_ServicePoints
WHERE lt_Beacon_Businesses_ServicePointID =
<cfqueryparam cfsqltype="cf_sql_integer" value="#RelID#">
AND BusinessID =
<cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
LIMIT 1
UPDATE ServicePoints
SET BeaconID = NULL,
AssignedByUserID = NULL
WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#ServicePointID#">
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
</cfquery>
<cfoutput>#serializeJSON({
"OK"=true,
"ERROR"="",
"ACTION"="deleted",
"lt_Beacon_Businesses_ServicePointID"=RelID,
"BeaconID"=qFind.BeaconID,
"ServicePointID"=qFind.ServicePointID,
"ACTION"="unassigned",
"ServicePointID"=ServicePointID,
"BeaconID"=removedBeaconID,
"BusinessID"=(request.BusinessID & "")
})#</cfoutput>

View file

@ -17,32 +17,29 @@ if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) ||
<cfquery name="q" datasource="payfrit">
SELECT
lt.lt_Beacon_Businesses_ServicePointID,
lt.BeaconID,
lt.BusinessID,
lt.ServicePointID,
lt.lt_Beacon_Businesses_ServicePointNotes,
b.BeaconName,
b.BeaconUUID,
sp.ServicePointName
FROM lt_Beacon_Businesses_ServicePoints lt
JOIN Beacons b ON b.BeaconID = lt.BeaconID
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
WHERE lt.BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
ORDER BY b.BeaconName, sp.ServicePointName
sp.ID AS ServicePointID,
sp.BeaconID,
sp.BusinessID,
sp.AssignedByUserID,
b.Name AS BeaconName,
b.UUID,
sp.Name AS ServicePointName
FROM ServicePoints sp
JOIN Beacons b ON b.ID = sp.BeaconID
WHERE sp.BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
AND sp.BeaconID IS NOT NULL
ORDER BY b.Name, sp.Name
</cfquery>
<cfset assignments = []>
<cfloop query="q">
<cfset arrayAppend(assignments, {
"lt_Beacon_Businesses_ServicePointID" = q.lt_Beacon_Businesses_ServicePointID,
"ServicePointID" = q.ServicePointID,
"BeaconID" = q.BeaconID,
"BusinessID" = q.BusinessID,
"ServicePointID" = q.ServicePointID,
"BeaconName" = q.BeaconName,
"BeaconUUID" = q.BeaconUUID,
"ServicePointName"= q.ServicePointName,
"lt_Beacon_Businesses_ServicePointNotes" = q.lt_Beacon_Businesses_ServicePointNotes
"UUID" = q.UUID,
"ServicePointName"= q.ServicePointName
})>
</cfloop>

Some files were not shown because too many files have changed in this diff Show more