Add takeaway/pickup order support
- getOrCreateCart: only require ServicePointID for dine-in (OrderTypeID=1) - get.cfm + items.cfm: return OrderTypes from Businesses table - saveOrderTypes.cfm: new endpoint to save business order type config - KDS: add PICKUP/DELIVERY badges on order cards - Portal: add Order Types toggle card in settings (Dine-In always on, Takeaway toggle) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b0fa48ab64
commit
6cdbff129f
8 changed files with 114 additions and 8 deletions
|
|
@ -53,7 +53,8 @@ try {
|
|||
TabMaxAuthAmount,
|
||||
TabAutoIncreaseThreshold,
|
||||
TabMaxMembers,
|
||||
TabApprovalRequired
|
||||
TabApprovalRequired,
|
||||
OrderTypes
|
||||
FROM Businesses
|
||||
WHERE ID = :businessID
|
||||
", { businessID: businessID }, { datasource: "payfrit" });
|
||||
|
|
@ -160,7 +161,8 @@ try {
|
|||
"TabMaxAuthAmount": isNumeric(q.TabMaxAuthAmount) ? q.TabMaxAuthAmount : 1000.00,
|
||||
"TabAutoIncreaseThreshold": isNumeric(q.TabAutoIncreaseThreshold) ? q.TabAutoIncreaseThreshold : 0.80,
|
||||
"TabMaxMembers": isNumeric(q.TabMaxMembers) ? q.TabMaxMembers : 10,
|
||||
"TabApprovalRequired": isNumeric(q.TabApprovalRequired) ? q.TabApprovalRequired : 1
|
||||
"TabApprovalRequired": isNumeric(q.TabApprovalRequired) ? q.TabApprovalRequired : 1,
|
||||
"OrderTypes": len(q.OrderTypes) ? q.OrderTypes : "1"
|
||||
};
|
||||
|
||||
// Add header image URL if extension exists
|
||||
|
|
|
|||
51
api/businesses/saveOrderTypes.cfm
Normal file
51
api/businesses/saveOrderTypes.cfm
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
<cfheader name="Cache-Control" value="no-store">
|
||||
|
||||
<cfscript>
|
||||
/**
|
||||
* Save Business Order Types
|
||||
* POST JSON: { "BusinessID": 37, "OrderTypes": "1,2" }
|
||||
* OrderTypes is a comma-separated list: 1=Dine-In, 2=Takeaway, 3=Delivery
|
||||
*/
|
||||
|
||||
response = { "OK": false };
|
||||
|
||||
try {
|
||||
requestBody = toString(getHttpRequestData().content);
|
||||
if (!len(requestBody)) {
|
||||
throw(message="No request body provided");
|
||||
}
|
||||
|
||||
data = deserializeJSON(requestBody);
|
||||
|
||||
businessId = structKeyExists(data, "BusinessID") ? val(data.BusinessID) : 0;
|
||||
if (businessId == 0) {
|
||||
throw(message="BusinessID is required");
|
||||
}
|
||||
|
||||
orderTypes = structKeyExists(data, "OrderTypes") && isSimpleValue(data.OrderTypes) ? trim(data.OrderTypes) : "1";
|
||||
|
||||
// Validate: only allow digits 1-3 separated by commas
|
||||
if (!reFind("^[1-3](,[1-3])*$", orderTypes)) {
|
||||
throw(message="OrderTypes must be a comma-separated list of 1, 2, or 3");
|
||||
}
|
||||
|
||||
queryTimed("
|
||||
UPDATE Businesses SET OrderTypes = :orderTypes
|
||||
WHERE ID = :bizId
|
||||
", {
|
||||
orderTypes: { value: orderTypes, cfsqltype: "cf_sql_varchar" },
|
||||
bizId: { value: businessId, cfsqltype: "cf_sql_integer" }
|
||||
}, { datasource: "payfrit" });
|
||||
|
||||
response.OK = true;
|
||||
response.OrderTypes = orderTypes;
|
||||
|
||||
} catch (any e) {
|
||||
response.ERROR = e.message;
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON(response));
|
||||
</cfscript>
|
||||
|
|
@ -558,7 +558,7 @@
|
|||
<cfset brandColor = "">
|
||||
<cfset headerImageUrl = "">
|
||||
<cfset qBrand = queryTimed(
|
||||
"SELECT BrandColor AS BusinessBrandColor, BrandColorLight AS BusinessBrandColorLight, TaxRate, PayfritFee, HeaderImageExtension, SessionEnabled FROM Businesses WHERE ID = ?",
|
||||
"SELECT BrandColor AS BusinessBrandColor, BrandColorLight AS BusinessBrandColorLight, TaxRate, PayfritFee, HeaderImageExtension, SessionEnabled, OrderTypes FROM Businesses WHERE ID = ?",
|
||||
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
|
||||
{ datasource = "payfrit" }
|
||||
)>
|
||||
|
|
@ -598,7 +598,8 @@
|
|||
"PAYFRITFEE": val(businessPayfritFee),
|
||||
"SESSIONENABLED": val(qBrand.SessionEnabled),
|
||||
"Menus": menuList,
|
||||
"SelectedMenuID": requestedMenuID
|
||||
"SelectedMenuID": requestedMenuID,
|
||||
"ORDERTYPES": len(qBrand.OrderTypes) ? qBrand.OrderTypes : "1"
|
||||
})>
|
||||
|
||||
<cfcatch>
|
||||
|
|
|
|||
|
|
@ -175,12 +175,12 @@
|
|||
})>
|
||||
</cfif>
|
||||
|
||||
<!--- Require ServicePointID for now (delivery/takeaway not yet supported) --->
|
||||
<cfif ServicePointID LTE 0>
|
||||
<!--- Require ServicePointID only for dine-in orders --->
|
||||
<cfif OrderTypeID EQ 1 AND ServicePointID LTE 0>
|
||||
<cfset apiAbort({
|
||||
"OK": false,
|
||||
"ERROR": "missing_service_point",
|
||||
"MESSAGE": "ServicePointID is required. Please scan a table beacon.",
|
||||
"MESSAGE": "ServicePointID is required for dine-in. Please scan a table beacon.",
|
||||
"DETAIL": ""
|
||||
})>
|
||||
</cfif>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@
|
|||
.station-btn svg { width: 24px; height: 24px; opacity: 0.5; }
|
||||
.station-name-display { font-size: 11px; color: #555; margin-left: 8px; text-transform: uppercase; letter-spacing: 1px; }
|
||||
|
||||
/* Order type badges */
|
||||
.badge-pickup { display: inline-block; font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px; background: #1a2a00; color: #a3e635; border: 1px solid #a3e63533; vertical-align: middle; margin-left: 6px; letter-spacing: 1px; }
|
||||
.badge-delivery { display: inline-block; font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px; background: #2a1a00; color: #fb923c; border: 1px solid #fb923c33; vertical-align: middle; margin-left: 6px; letter-spacing: 1px; }
|
||||
|
||||
/* Expand toggle */
|
||||
.expand-toggle { background: none; border: 1px solid #333; color: #555; padding: 4px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; margin-bottom: 10px; width: 100%; transition: all 0.2s; }
|
||||
.expand-toggle:hover { border-color: #555; color: #888; }
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ function renderOrder(order) {
|
|||
return `
|
||||
<div class="order-card ${statusClass}">
|
||||
<div class="order-header">
|
||||
<div class="order-number">#${order.OrderID}</div>
|
||||
<div class="order-number">#${order.OrderID}${order.OrderTypeID === 2 ? ' <span class="badge-pickup">PICKUP</span>' : ''}${order.OrderTypeID === 3 ? ' <span class="badge-delivery">DELIVERY</span>' : ''}</div>
|
||||
<div class="order-time">
|
||||
<div class="elapsed-time ${timeClass}">${formatElapsedTime(elapsedTime)}</div>
|
||||
<div class="submit-time">${formatSubmitTime(order.SubmittedOn)}</div>
|
||||
|
|
|
|||
|
|
@ -689,6 +689,31 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Order Types</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p style="color: #666; font-size: 13px; margin-bottom: 16px;">Choose which order types your business supports. Dine-in is always enabled.</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="orderTypeDineIn" checked disabled>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<span>Dine-In</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="orderTypeTakeaway" onchange="Portal.saveOrderTypes()">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<span>Takeaway / Pickup</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Tabs / Running Checks</h3>
|
||||
|
|
|
|||
|
|
@ -864,6 +864,11 @@ const Portal = {
|
|||
if (thresholdInput) thresholdInput.value = biz.TABAUTOINCREASETHRESHOLD || biz.TabAutoIncreaseThreshold || 0.80;
|
||||
const approvalCheckbox = document.getElementById('tabApprovalRequired');
|
||||
if (approvalCheckbox) approvalCheckbox.checked = (biz.TABAPPROVALREQUIRED || biz.TabApprovalRequired || 1) == 1;
|
||||
|
||||
// Order types
|
||||
const orderTypes = (biz.ORDERTYPES || biz.OrderTypes || '1').split(',');
|
||||
const takeawayCheckbox = document.getElementById('orderTypeTakeaway');
|
||||
if (takeawayCheckbox) takeawayCheckbox.checked = orderTypes.includes('2');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Portal] Error loading business info:', err);
|
||||
|
|
@ -901,6 +906,24 @@ const Portal = {
|
|||
}
|
||||
},
|
||||
|
||||
async saveOrderTypes() {
|
||||
const types = ['1']; // Dine-in always enabled
|
||||
if (document.getElementById('orderTypeTakeaway').checked) types.push('2');
|
||||
try {
|
||||
const response = await fetch(`${this.config.apiBaseUrl}/businesses/saveOrderTypes.cfm`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ BusinessID: this.config.businessId, OrderTypes: types.join(',') })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.OK) { this.showToast('Order types saved!', 'success'); }
|
||||
else { this.showToast(data.ERROR || 'Failed to save order types', 'error'); }
|
||||
} catch (err) {
|
||||
console.error('[Portal] Error saving order types:', err);
|
||||
this.showToast('Error saving order types', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// Render hours editor
|
||||
renderHoursEditor(hours) {
|
||||
const container = document.getElementById('hoursEditor');
|
||||
|
|
|
|||
Reference in a new issue