Features: - Multi-menu support with time-based availability - Menu hours validation against business operating hours - Setup wizard now creates Menu records and links categories - New menus.cfm API for menu CRUD operations - Category schedule filtering (day/time based visibility) - Beacon UUID lookup API for customer app - Parent/child business relationships for franchises - Category listing API for menu builder Portal improvements: - Menu builder theming to match admin UI - Brand color picker fix - Header image preview improvements API fixes: - Filter demo/hidden businesses from restaurant list - Improved error handling throughout Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
204 lines
7.2 KiB
Text
204 lines
7.2 KiB
Text
<cfsetting showdebugoutput="false">
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Address Geocoding</title>
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 1000px; margin: 40px auto; padding: 20px; background: ##1a1a1a; color: ##fff; }
|
|
h1 { color: ##4CAF50; }
|
|
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
|
th, td { padding: 10px; text-align: left; border-bottom: 1px solid ##333; }
|
|
th { background: ##2a2a2a; }
|
|
tr:hover { background: ##2a2a2a; }
|
|
.address { color: ##aaa; font-size: 13px; }
|
|
.success { color: ##4CAF50; padding: 10px; background: ##1b3d1b; border-radius: 4px; margin: 10px 0; }
|
|
.error { color: ##f44336; padding: 10px; background: ##3d1b1b; border-radius: 4px; margin: 10px 0; }
|
|
.has-coords { color: ##4CAF50; }
|
|
.no-coords { color: ##ff9800; }
|
|
button { padding: 6px 12px; background: ##4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
|
button:hover { background: ##45a049; }
|
|
.btn-lookup { background: ##2196F3; }
|
|
.btn-lookup:hover { background: ##1976D2; }
|
|
.coords { font-family: monospace; font-size: 12px; color: ##888; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Address Geocoding</h1>
|
|
<p>Auto-geocode addresses using OpenStreetMap Nominatim (free, no API key required)</p>
|
|
|
|
<cfscript>
|
|
function geocodeAddress(addressString) {
|
|
var result = { "success": false, "error": "" };
|
|
if (len(trim(addressString)) EQ 0) {
|
|
result.error = "Empty address";
|
|
return result;
|
|
}
|
|
|
|
try {
|
|
var httpService = new http();
|
|
httpService.setMethod("GET");
|
|
httpService.setUrl("https://nominatim.openstreetmap.org/search?q=" & urlEncodedFormat(addressString) & "&format=json&limit=1");
|
|
httpService.addParam(type="header", name="User-Agent", value="Payfrit/1.0");
|
|
httpService.setTimeout(10);
|
|
|
|
var httpResult = httpService.send().getPrefix();
|
|
|
|
if (httpResult.statusCode CONTAINS "200") {
|
|
var data = deserializeJSON(httpResult.fileContent);
|
|
if (arrayLen(data) GT 0) {
|
|
result.success = true;
|
|
result.lat = data[1].lat;
|
|
result.lng = data[1].lon;
|
|
return result;
|
|
}
|
|
result.error = "No results found";
|
|
return result;
|
|
}
|
|
result.error = "HTTP " & httpResult.statusCode;
|
|
return result;
|
|
} catch (any e) {
|
|
result.error = e.message;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
function buildAddressString(line1, line2, city, zipCode) {
|
|
var parts = [];
|
|
if (len(trim(line1))) arrayAppend(parts, trim(line1));
|
|
if (len(trim(line2))) arrayAppend(parts, trim(line2));
|
|
if (len(trim(city))) arrayAppend(parts, trim(city));
|
|
if (len(trim(zipCode))) arrayAppend(parts, trim(zipCode));
|
|
return arrayToList(parts, ", ");
|
|
}
|
|
</cfscript>
|
|
|
|
<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">
|
|
</cfquery>
|
|
<cfif addr.recordCount GT 0>
|
|
<cfset fullAddress = buildAddressString(addr.AddressLine1, addr.AddressLine2, addr.AddressCity, addr.AddressZIPCode)>
|
|
<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">
|
|
</cfquery>
|
|
<cfoutput><div class="success">Geocoded Address ID #addressId#: #geo.lat#, #geo.lng#</div></cfoutput>
|
|
<cfelse>
|
|
<cfoutput><div class="error">Failed: #geo.error#</div></cfoutput>
|
|
</cfif>
|
|
</cfif>
|
|
</cfif>
|
|
|
|
<cfif structKeyExists(url, "geocodeAll")>
|
|
<cfquery name="missing" datasource="payfrit">
|
|
SELECT AddressID, AddressLine1, AddressLine2, AddressCity, AddressZIPCode
|
|
FROM Addresses
|
|
WHERE (AddressLat IS NULL OR AddressLat = 0)
|
|
AND AddressLine1 IS NOT NULL AND AddressLine1 != ''
|
|
</cfquery>
|
|
<cfset successCount = 0>
|
|
<cfset failCount = 0>
|
|
<cfloop query="missing">
|
|
<cfset fullAddress = buildAddressString(missing.AddressLine1, missing.AddressLine2, missing.AddressCity, missing.AddressZIPCode)>
|
|
<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">
|
|
</cfquery>
|
|
<cfset successCount = successCount + 1>
|
|
<cfelse>
|
|
<cfset failCount = failCount + 1>
|
|
</cfif>
|
|
<cfset sleep(1100)>
|
|
</cfloop>
|
|
<cfoutput><div class="success">Geocoded #successCount# addresses. #failCount# failed.</div></cfoutput>
|
|
</cfif>
|
|
|
|
<cfquery name="addresses" datasource="payfrit">
|
|
SELECT
|
|
b.BusinessID,
|
|
b.BusinessName,
|
|
a.AddressID,
|
|
a.AddressLine1,
|
|
a.AddressLine2,
|
|
a.AddressCity,
|
|
a.AddressZIPCode,
|
|
a.AddressLat,
|
|
a.AddressLng
|
|
FROM Businesses b
|
|
LEFT JOIN Addresses a ON b.BusinessAddressID = a.AddressID
|
|
ORDER BY b.BusinessName
|
|
</cfquery>
|
|
|
|
<cfset missingCount = 0>
|
|
<cfloop query="addresses">
|
|
<cfif (NOT len(addresses.AddressLat) OR val(addresses.AddressLat) EQ 0) AND len(addresses.AddressLine1)>
|
|
<cfset missingCount = missingCount + 1>
|
|
</cfif>
|
|
</cfloop>
|
|
|
|
<cfoutput>
|
|
<p>
|
|
<strong>#missingCount#</strong> addresses missing coordinates.
|
|
<cfif missingCount GT 0>
|
|
<a href="?geocodeAll=1"><button class="btn-lookup">Geocode All Missing (#missingCount#)</button></a>
|
|
<small style="color:##888;">(~#missingCount# seconds due to rate limiting)</small>
|
|
</cfif>
|
|
</p>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Business</th>
|
|
<th>Address</th>
|
|
<th>Coordinates</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<cfloop query="addresses">
|
|
<tr>
|
|
<td>
|
|
#addresses.BusinessName#
|
|
<cfif len(addresses.AddressLat) AND val(addresses.AddressLat) NEQ 0>
|
|
<span class="has-coords">#chr(10003)#</span>
|
|
<cfelseif len(addresses.AddressLine1)>
|
|
<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#
|
|
<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, "_.______")#
|
|
<cfelse>
|
|
-
|
|
</cfif>
|
|
</td>
|
|
<td>
|
|
<cfif len(addresses.AddressLine1) AND len(addresses.AddressID)>
|
|
<a href="?geocode=1&addressId=#addresses.AddressID#"><button class="btn-lookup">Lookup</button></a>
|
|
</cfif>
|
|
</td>
|
|
</tr>
|
|
</cfloop>
|
|
</tbody>
|
|
</table>
|
|
</cfoutput>
|
|
|
|
</body>
|
|
</html>
|