feat: per-station item completion on KDS
Each station can mark their items done independently. When all stations are done, the order auto-promotes to Ready (status 3) and delivery/pickup tasks are created automatically. - New markStationDone.cfm endpoint for per-station completion - Extract task creation into shared _createOrderTasks.cfm include - Add line item StatusID to listForKDS.cfm response - KDS shows per-station "Mark Station Done" button when filtered - Done items display with strikethrough and checkmark - Manager view retains full manual control (no station selected) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f3a41bf01a
commit
d822fcad5a
6 changed files with 385 additions and 162 deletions
171
api/orders/_createOrderTasks.cfm
Normal file
171
api/orders/_createOrderTasks.cfm
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
<!---
|
||||||
|
Shared task creation logic for orders moving to status 3 (Ready).
|
||||||
|
Expects these variables in calling scope:
|
||||||
|
- OrderID (integer)
|
||||||
|
- qOrder (query with BusinessID, ServicePointID, Name)
|
||||||
|
- NewStatusID (integer)
|
||||||
|
- oldStatusID (integer)
|
||||||
|
Sets:
|
||||||
|
- taskCreated (boolean)
|
||||||
|
- cashTaskCreated (boolean)
|
||||||
|
--->
|
||||||
|
|
||||||
|
<cfset taskCreated = false>
|
||||||
|
<cfset cashTaskCreated = false>
|
||||||
|
<cfif NewStatusID EQ 3 AND oldStatusID NEQ 3>
|
||||||
|
<cftry>
|
||||||
|
<!--- Get order type --->
|
||||||
|
<cfset qOrderDetails = queryExecute("
|
||||||
|
SELECT o.OrderTypeID
|
||||||
|
FROM Orders o
|
||||||
|
WHERE o.ID = ?
|
||||||
|
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<cfset orderTypeID = qOrderDetails.recordCount GT 0 ? val(qOrderDetails.OrderTypeID) : 1>
|
||||||
|
|
||||||
|
<!--- Look up task type for this business based on order type --->
|
||||||
|
<!--- OrderTypeID: 1=dine-in, 2=takeaway, 3=delivery --->
|
||||||
|
<cfif orderTypeID EQ 1>
|
||||||
|
<cfset taskTypeName = "Deliver to Table">
|
||||||
|
<cfelseif orderTypeID EQ 2>
|
||||||
|
<cfset taskTypeName = "Order Ready for Pickup">
|
||||||
|
<cfelseif orderTypeID EQ 3>
|
||||||
|
<cfset taskTypeName = "Deliver to Address">
|
||||||
|
<cfelse>
|
||||||
|
<cfset taskTypeName = "">
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfset taskTypeID = 0>
|
||||||
|
<cfif len(taskTypeName)>
|
||||||
|
<cfset qTaskType = queryExecute("
|
||||||
|
SELECT ID FROM tt_TaskTypes WHERE BusinessID = ? AND Name = ? LIMIT 1
|
||||||
|
", [
|
||||||
|
{ value = qOrder.BusinessID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = taskTypeName, cfsqltype = "cf_sql_varchar" }
|
||||||
|
], { datasource = "payfrit" })>
|
||||||
|
<cfset taskTypeID = qTaskType.recordCount GT 0 ? qTaskType.ID : 0>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Check if this type of task already exists for this order to prevent duplicates --->
|
||||||
|
<cfset qExisting = queryExecute("
|
||||||
|
SELECT ID FROM Tasks WHERE OrderID = ? AND TaskTypeID = ? LIMIT 1
|
||||||
|
", [
|
||||||
|
{ value = OrderID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = taskTypeID, cfsqltype = "cf_sql_integer" }
|
||||||
|
], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<cfif qExisting.recordCount EQ 0 AND taskTypeID GT 0>
|
||||||
|
<!--- Build task title based on order type --->
|
||||||
|
<cfif orderTypeID EQ 1>
|
||||||
|
<!--- Dine-in: Server delivers to service point --->
|
||||||
|
<cfset tableName = len(qOrder.Name) ? qOrder.Name : "Table">
|
||||||
|
<cfset taskTitle = "Deliver Order ###OrderID# to " & tableName>
|
||||||
|
<cfset taskCategoryID = 3>
|
||||||
|
<cfelseif orderTypeID EQ 2>
|
||||||
|
<!--- Takeaway: Expo task to notify customer order is ready --->
|
||||||
|
<cfset taskTitle = "Order ###OrderID# Ready for Pickup">
|
||||||
|
<cfset taskCategoryID = 4>
|
||||||
|
<cfelseif orderTypeID EQ 3>
|
||||||
|
<!--- Delivery: Hand off to delivery driver --->
|
||||||
|
<cfset taskTitle = "Deliver Order ###OrderID# to Address">
|
||||||
|
<cfset taskCategoryID = 5>
|
||||||
|
<cfelse>
|
||||||
|
<cfset taskTitle = "">
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfif len(taskTitle)>
|
||||||
|
<cfset queryExecute("
|
||||||
|
INSERT INTO Tasks (
|
||||||
|
BusinessID,
|
||||||
|
OrderID,
|
||||||
|
ServicePointID,
|
||||||
|
TaskTypeID,
|
||||||
|
CategoryID,
|
||||||
|
Title,
|
||||||
|
ClaimedByUserID,
|
||||||
|
CreatedOn
|
||||||
|
) VALUES (
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
0,
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
", [
|
||||||
|
{ value = qOrder.BusinessID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = OrderID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = qOrder.ServicePointID, cfsqltype = "cf_sql_integer", null = isNull(qOrder.ServicePointID) OR val(qOrder.ServicePointID) EQ 0 },
|
||||||
|
{ value = taskTypeID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = taskCategoryID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = taskTitle, cfsqltype = "cf_sql_varchar" }
|
||||||
|
], { datasource = "payfrit" })>
|
||||||
|
<cfset taskCreated = true>
|
||||||
|
</cfif>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Check for pending cash payment and create "Pay With Cash" task --->
|
||||||
|
<cfset qCashPayment = queryExecute("
|
||||||
|
SELECT p.PaymentPaidInCash, o.PaymentStatus, o.ServicePointID, sp.Name AS ServicePointName
|
||||||
|
FROM Orders o
|
||||||
|
LEFT JOIN Payments p ON p.PaymentID = o.PaymentID
|
||||||
|
LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID
|
||||||
|
WHERE o.ID = ?
|
||||||
|
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<cfif qCashPayment.recordCount GT 0 AND val(qCashPayment.PaymentPaidInCash) GT 0 AND qCashPayment.PaymentStatus EQ "pending">
|
||||||
|
<!--- Check if there's already an active cash task for this order --->
|
||||||
|
<cfset qExistingCashTask = queryExecute("
|
||||||
|
SELECT t.ID FROM Tasks t
|
||||||
|
INNER JOIN tt_TaskTypes tt ON tt.ID = t.TaskTypeID
|
||||||
|
WHERE t.OrderID = ? AND tt.Name LIKE '%Cash%' AND t.CompletedOn IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<cfif qExistingCashTask.recordCount EQ 0>
|
||||||
|
<!--- Get "Pay With Cash" task type ID for this business --->
|
||||||
|
<cfset qCashTaskType = queryExecute("
|
||||||
|
SELECT ID FROM tt_TaskTypes WHERE BusinessID = ? AND Name LIKE '%Cash%' LIMIT 1
|
||||||
|
", [ { value = qOrder.BusinessID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<cfset cashTaskTypeID = qCashTaskType.recordCount GT 0 ? qCashTaskType.ID : 0>
|
||||||
|
<cfset cashTaskTitle = "Pay With Cash - Order ###OrderID#">
|
||||||
|
<cfif len(qCashPayment.ServicePointName)>
|
||||||
|
<cfset cashTaskTitle = cashTaskTitle & " (" & qCashPayment.ServicePointName & ")">
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfset queryExecute("
|
||||||
|
INSERT INTO Tasks (
|
||||||
|
BusinessID,
|
||||||
|
OrderID,
|
||||||
|
TaskTypeID,
|
||||||
|
Title,
|
||||||
|
ClaimedByUserID,
|
||||||
|
CreatedOn,
|
||||||
|
ServicePointID
|
||||||
|
) VALUES (
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
0,
|
||||||
|
NOW(),
|
||||||
|
?
|
||||||
|
)
|
||||||
|
", [
|
||||||
|
{ value = qOrder.BusinessID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = OrderID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = cashTaskTypeID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = cashTaskTitle, cfsqltype = "cf_sql_varchar" },
|
||||||
|
{ value = qCashPayment.ServicePointID, cfsqltype = "cf_sql_integer", null = isNull(qCashPayment.ServicePointID) OR val(qCashPayment.ServicePointID) EQ 0 }
|
||||||
|
], { datasource = "payfrit" })>
|
||||||
|
<cfset cashTaskCreated = true>
|
||||||
|
</cfif>
|
||||||
|
</cfif>
|
||||||
|
<cfcatch>
|
||||||
|
<!--- Task creation failed, but don't fail the status update --->
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
</cfif>
|
||||||
|
|
@ -120,6 +120,7 @@
|
||||||
oli.Quantity,
|
oli.Quantity,
|
||||||
oli.Remark,
|
oli.Remark,
|
||||||
oli.IsDeleted,
|
oli.IsDeleted,
|
||||||
|
oli.StatusID,
|
||||||
i.Name,
|
i.Name,
|
||||||
i.ParentItemID,
|
i.ParentItemID,
|
||||||
i.IsCheckedByDefault,
|
i.IsCheckedByDefault,
|
||||||
|
|
@ -146,7 +147,8 @@
|
||||||
"ParentItemID": qLineItems.ParentItemID,
|
"ParentItemID": qLineItems.ParentItemID,
|
||||||
"ItemParentName": qLineItems.ItemParentName,
|
"ItemParentName": qLineItems.ItemParentName,
|
||||||
"IsCheckedByDefault": qLineItems.IsCheckedByDefault,
|
"IsCheckedByDefault": qLineItems.IsCheckedByDefault,
|
||||||
"StationID": qLineItems.StationID
|
"StationID": qLineItems.StationID,
|
||||||
|
"StatusID": val(qLineItems.StatusID)
|
||||||
})>
|
})>
|
||||||
</cfloop>
|
</cfloop>
|
||||||
|
|
||||||
|
|
|
||||||
149
api/orders/markStationDone.cfm
Normal file
149
api/orders/markStationDone.cfm
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
|
||||||
|
<cffunction name="apiAbort" access="public" returntype="void" output="true">
|
||||||
|
<cfargument name="payload" type="struct" required="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8">
|
||||||
|
<cfoutput>#serializeJSON(arguments.payload)#</cfoutput>
|
||||||
|
<cfabort>
|
||||||
|
</cffunction>
|
||||||
|
|
||||||
|
<cffunction name="readJsonBody" access="public" returntype="struct" output="false">
|
||||||
|
<cfset var raw = getHttpRequestData().content>
|
||||||
|
<cfif isNull(raw) OR len(trim(raw)) EQ 0>
|
||||||
|
<cfreturn {}>
|
||||||
|
</cfif>
|
||||||
|
<cftry>
|
||||||
|
<cfset var data = deserializeJSON(raw)>
|
||||||
|
<cfif isStruct(data)>
|
||||||
|
<cfreturn data>
|
||||||
|
<cfelse>
|
||||||
|
<cfreturn {}>
|
||||||
|
</cfif>
|
||||||
|
<cfcatch>
|
||||||
|
<cfreturn {}>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
</cffunction>
|
||||||
|
|
||||||
|
<cfset data = readJsonBody()>
|
||||||
|
<cfset OrderID = val( structKeyExists(data,"OrderID") ? data.OrderID : 0 )>
|
||||||
|
<cfset StationID = val( structKeyExists(data,"StationID") ? data.StationID : 0 )>
|
||||||
|
|
||||||
|
<cfif OrderID LTE 0 OR StationID LTE 0>
|
||||||
|
<cfset apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "OrderID and StationID are required.", "DETAIL": "" })>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cftry>
|
||||||
|
<!--- Verify order exists and get details --->
|
||||||
|
<cfset qOrder = queryExecute("
|
||||||
|
SELECT o.ID, o.StatusID, o.BusinessID, o.ServicePointID,
|
||||||
|
sp.Name
|
||||||
|
FROM Orders o
|
||||||
|
LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID
|
||||||
|
WHERE o.ID = ?
|
||||||
|
LIMIT 1
|
||||||
|
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<cfif qOrder.recordCount EQ 0>
|
||||||
|
<cfset apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Order not found.", "DETAIL": "" })>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfset oldStatusID = qOrder.StatusID>
|
||||||
|
|
||||||
|
<!--- Mark root line items for this station as done (StatusID = 1) --->
|
||||||
|
<cfset queryExecute("
|
||||||
|
UPDATE OrderLineItems oli
|
||||||
|
INNER JOIN Items i ON i.ID = oli.ItemID
|
||||||
|
SET oli.StatusID = 1
|
||||||
|
WHERE oli.OrderID = ?
|
||||||
|
AND i.StationID = ?
|
||||||
|
AND oli.ParentOrderLineItemID = 0
|
||||||
|
AND oli.IsDeleted = b'0'
|
||||||
|
", [
|
||||||
|
{ value = OrderID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = StationID, cfsqltype = "cf_sql_integer" }
|
||||||
|
], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<!--- Also mark children (modifiers) of those root items as done --->
|
||||||
|
<cfset queryExecute("
|
||||||
|
UPDATE OrderLineItems oli
|
||||||
|
SET oli.StatusID = 1
|
||||||
|
WHERE oli.OrderID = ?
|
||||||
|
AND oli.IsDeleted = b'0'
|
||||||
|
AND oli.ParentOrderLineItemID IN (
|
||||||
|
SELECT sub.ID FROM (
|
||||||
|
SELECT oli2.ID
|
||||||
|
FROM OrderLineItems oli2
|
||||||
|
INNER JOIN Items i2 ON i2.ID = oli2.ItemID
|
||||||
|
WHERE oli2.OrderID = ?
|
||||||
|
AND i2.StationID = ?
|
||||||
|
AND oli2.ParentOrderLineItemID = 0
|
||||||
|
AND oli2.IsDeleted = b'0'
|
||||||
|
) sub
|
||||||
|
)
|
||||||
|
", [
|
||||||
|
{ value = OrderID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = OrderID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = StationID, cfsqltype = "cf_sql_integer" }
|
||||||
|
], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<!--- If order was at status 1 (Submitted), auto-promote to status 2 (Preparing) --->
|
||||||
|
<cfif oldStatusID EQ 1>
|
||||||
|
<cfset queryExecute("
|
||||||
|
UPDATE Orders
|
||||||
|
SET StatusID = 2, LastEditedOn = NOW()
|
||||||
|
WHERE ID = ?
|
||||||
|
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
<cfset oldStatusID = 2>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Check if ALL station-assigned root items are now done --->
|
||||||
|
<cfset qRemaining = queryExecute("
|
||||||
|
SELECT COUNT(*) AS RemainingCount
|
||||||
|
FROM OrderLineItems oli
|
||||||
|
INNER JOIN Items i ON i.ID = oli.ItemID
|
||||||
|
WHERE oli.OrderID = ?
|
||||||
|
AND oli.ParentOrderLineItemID = 0
|
||||||
|
AND oli.IsDeleted = b'0'
|
||||||
|
AND i.StationID > 0
|
||||||
|
AND oli.StatusID = 0
|
||||||
|
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<cfset allStationsDone = val(qRemaining.RemainingCount) EQ 0>
|
||||||
|
<cfset NewStatusID = oldStatusID>
|
||||||
|
|
||||||
|
<!--- If all stations done, auto-promote to status 3 (Ready) and create tasks --->
|
||||||
|
<cfif allStationsDone AND oldStatusID LT 3>
|
||||||
|
<cfset NewStatusID = 3>
|
||||||
|
<cfset queryExecute("
|
||||||
|
UPDATE Orders
|
||||||
|
SET StatusID = 3, LastEditedOn = NOW()
|
||||||
|
WHERE ID = ?
|
||||||
|
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
|
<!--- Create delivery/pickup tasks via shared include --->
|
||||||
|
<cfinclude template="_createOrderTasks.cfm">
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfset apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"ERROR": "",
|
||||||
|
"MESSAGE": "Station items marked as done.",
|
||||||
|
"OrderID": OrderID,
|
||||||
|
"StationID": StationID,
|
||||||
|
"StationDone": true,
|
||||||
|
"AllStationsDone": allStationsDone,
|
||||||
|
"NewOrderStatusID": NewStatusID,
|
||||||
|
"TaskCreated": allStationsDone AND isDefined("taskCreated") AND taskCreated
|
||||||
|
})>
|
||||||
|
|
||||||
|
<cfcatch>
|
||||||
|
<cfset apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": "Error marking station done",
|
||||||
|
"DETAIL": cfcatch.message
|
||||||
|
})>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
|
@ -86,165 +86,7 @@
|
||||||
--->
|
--->
|
||||||
|
|
||||||
<!--- Create delivery/pickup task when order moves to status 3 (Final Prep) --->
|
<!--- Create delivery/pickup task when order moves to status 3 (Final Prep) --->
|
||||||
<cfset taskCreated = false>
|
<cfinclude template="_createOrderTasks.cfm">
|
||||||
<cfset cashTaskCreated = false>
|
|
||||||
<cfif NewStatusID EQ 3 AND oldStatusID NEQ 3>
|
|
||||||
<cftry>
|
|
||||||
<!--- Get order type --->
|
|
||||||
<cfset qOrderDetails = queryExecute("
|
|
||||||
SELECT o.OrderTypeID
|
|
||||||
FROM Orders o
|
|
||||||
WHERE o.ID = ?
|
|
||||||
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
|
||||||
|
|
||||||
<cfset orderTypeID = qOrderDetails.recordCount GT 0 ? val(qOrderDetails.OrderTypeID) : 1>
|
|
||||||
|
|
||||||
<!--- Look up task type for this business based on order type --->
|
|
||||||
<!--- OrderTypeID: 1=dine-in, 2=takeaway, 3=delivery --->
|
|
||||||
<cfif orderTypeID EQ 1>
|
|
||||||
<cfset taskTypeName = "Deliver to Table">
|
|
||||||
<cfelseif orderTypeID EQ 2>
|
|
||||||
<cfset taskTypeName = "Order Ready for Pickup">
|
|
||||||
<cfelseif orderTypeID EQ 3>
|
|
||||||
<cfset taskTypeName = "Deliver to Address">
|
|
||||||
<cfelse>
|
|
||||||
<cfset taskTypeName = "">
|
|
||||||
</cfif>
|
|
||||||
|
|
||||||
<cfset taskTypeID = 0>
|
|
||||||
<cfif len(taskTypeName)>
|
|
||||||
<cfset qTaskType = queryExecute("
|
|
||||||
SELECT ID FROM tt_TaskTypes WHERE BusinessID = ? AND Name = ? LIMIT 1
|
|
||||||
", [
|
|
||||||
{ value = qOrder.BusinessID, cfsqltype = "cf_sql_integer" },
|
|
||||||
{ value = taskTypeName, cfsqltype = "cf_sql_varchar" }
|
|
||||||
], { datasource = "payfrit" })>
|
|
||||||
<cfset taskTypeID = qTaskType.recordCount GT 0 ? qTaskType.ID : 0>
|
|
||||||
</cfif>
|
|
||||||
|
|
||||||
<!--- Check if this type of task already exists for this order to prevent duplicates --->
|
|
||||||
<cfset qExisting = queryExecute("
|
|
||||||
SELECT ID FROM Tasks WHERE OrderID = ? AND TaskTypeID = ? LIMIT 1
|
|
||||||
", [
|
|
||||||
{ value = OrderID, cfsqltype = "cf_sql_integer" },
|
|
||||||
{ value = taskTypeID, cfsqltype = "cf_sql_integer" }
|
|
||||||
], { datasource = "payfrit" })>
|
|
||||||
|
|
||||||
<cfif qExisting.recordCount EQ 0 AND taskTypeID GT 0>
|
|
||||||
<!--- Build task title based on order type --->
|
|
||||||
<cfif orderTypeID EQ 1>
|
|
||||||
<!--- Dine-in: Server delivers to service point --->
|
|
||||||
<cfset tableName = len(qOrder.Name) ? qOrder.Name : "Table">
|
|
||||||
<cfset taskTitle = "Deliver Order ###OrderID# to " & tableName>
|
|
||||||
<cfset taskCategoryID = 3>
|
|
||||||
<cfelseif orderTypeID EQ 2>
|
|
||||||
<!--- Takeaway: Expo task to notify customer order is ready --->
|
|
||||||
<cfset taskTitle = "Order ###OrderID# Ready for Pickup">
|
|
||||||
<cfset taskCategoryID = 4> <!--- Expo/Pickup category --->
|
|
||||||
<cfelseif orderTypeID EQ 3>
|
|
||||||
<!--- Delivery: Hand off to delivery driver --->
|
|
||||||
<cfset taskTitle = "Deliver Order ###OrderID# to Address">
|
|
||||||
<cfset taskCategoryID = 5> <!--- Delivery category --->
|
|
||||||
<cfelse>
|
|
||||||
<cfset taskTitle = "">
|
|
||||||
</cfif>
|
|
||||||
|
|
||||||
<cfif len(taskTitle)>
|
|
||||||
<cfset queryExecute("
|
|
||||||
INSERT INTO Tasks (
|
|
||||||
BusinessID,
|
|
||||||
OrderID,
|
|
||||||
ServicePointID,
|
|
||||||
TaskTypeID,
|
|
||||||
CategoryID,
|
|
||||||
Title,
|
|
||||||
ClaimedByUserID,
|
|
||||||
CreatedOn
|
|
||||||
) VALUES (
|
|
||||||
?,
|
|
||||||
?,
|
|
||||||
?,
|
|
||||||
?,
|
|
||||||
?,
|
|
||||||
?,
|
|
||||||
0,
|
|
||||||
NOW()
|
|
||||||
)
|
|
||||||
", [
|
|
||||||
{ value = qOrder.BusinessID, cfsqltype = "cf_sql_integer" },
|
|
||||||
{ value = OrderID, cfsqltype = "cf_sql_integer" },
|
|
||||||
{ value = qOrder.ServicePointID, cfsqltype = "cf_sql_integer", null = isNull(qOrder.ServicePointID) OR val(qOrder.ServicePointID) EQ 0 },
|
|
||||||
{ value = taskTypeID, cfsqltype = "cf_sql_integer" },
|
|
||||||
{ value = taskCategoryID, cfsqltype = "cf_sql_integer" },
|
|
||||||
{ value = taskTitle, cfsqltype = "cf_sql_varchar" }
|
|
||||||
], { datasource = "payfrit" })>
|
|
||||||
<cfset taskCreated = true>
|
|
||||||
</cfif>
|
|
||||||
</cfif>
|
|
||||||
|
|
||||||
<!--- Check for pending cash payment and create "Pay With Cash" task --->
|
|
||||||
<cfset qCashPayment = queryExecute("
|
|
||||||
SELECT p.PaymentPaidInCash, o.PaymentStatus, o.ServicePointID, sp.Name AS ServicePointName
|
|
||||||
FROM Orders o
|
|
||||||
LEFT JOIN Payments p ON p.PaymentID = o.PaymentID
|
|
||||||
LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID
|
|
||||||
WHERE o.ID = ?
|
|
||||||
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
|
||||||
|
|
||||||
<cfif qCashPayment.recordCount GT 0 AND val(qCashPayment.PaymentPaidInCash) GT 0 AND qCashPayment.PaymentStatus EQ "pending">
|
|
||||||
<!--- Check if there's already an active cash task for this order --->
|
|
||||||
<cfset qExistingCashTask = queryExecute("
|
|
||||||
SELECT t.ID FROM Tasks t
|
|
||||||
INNER JOIN tt_TaskTypes tt ON tt.ID = t.TaskTypeID
|
|
||||||
WHERE t.OrderID = ? AND tt.Name LIKE '%Cash%' AND t.CompletedOn IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
|
||||||
|
|
||||||
<cfif qExistingCashTask.recordCount EQ 0>
|
|
||||||
<!--- Get "Pay With Cash" task type ID for this business --->
|
|
||||||
<cfset qCashTaskType = queryExecute("
|
|
||||||
SELECT ID FROM tt_TaskTypes WHERE BusinessID = ? AND Name LIKE '%Cash%' LIMIT 1
|
|
||||||
", [ { value = qOrder.BusinessID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
|
||||||
|
|
||||||
<cfset cashTaskTypeID = qCashTaskType.recordCount GT 0 ? qCashTaskType.ID : 0>
|
|
||||||
<cfset cashTaskTitle = "Pay With Cash - Order ###OrderID#">
|
|
||||||
<cfif len(qCashPayment.ServicePointName)>
|
|
||||||
<cfset cashTaskTitle = cashTaskTitle & " (" & qCashPayment.ServicePointName & ")">
|
|
||||||
</cfif>
|
|
||||||
|
|
||||||
<cfset queryExecute("
|
|
||||||
INSERT INTO Tasks (
|
|
||||||
BusinessID,
|
|
||||||
OrderID,
|
|
||||||
TaskTypeID,
|
|
||||||
Title,
|
|
||||||
ClaimedByUserID,
|
|
||||||
CreatedOn,
|
|
||||||
ServicePointID
|
|
||||||
) VALUES (
|
|
||||||
?,
|
|
||||||
?,
|
|
||||||
?,
|
|
||||||
?,
|
|
||||||
0,
|
|
||||||
NOW(),
|
|
||||||
?
|
|
||||||
)
|
|
||||||
", [
|
|
||||||
{ value = qOrder.BusinessID, cfsqltype = "cf_sql_integer" },
|
|
||||||
{ value = OrderID, cfsqltype = "cf_sql_integer" },
|
|
||||||
{ value = cashTaskTypeID, cfsqltype = "cf_sql_integer" },
|
|
||||||
{ value = cashTaskTitle, cfsqltype = "cf_sql_varchar" },
|
|
||||||
{ value = qCashPayment.ServicePointID, cfsqltype = "cf_sql_integer", null = isNull(qCashPayment.ServicePointID) OR val(qCashPayment.ServicePointID) EQ 0 }
|
|
||||||
], { datasource = "payfrit" })>
|
|
||||||
<cfset cashTaskCreated = true>
|
|
||||||
</cfif><!--- qExistingCashTask.recordCount EQ 0 --->
|
|
||||||
</cfif>
|
|
||||||
<cfcatch>
|
|
||||||
<!--- Task creation failed, but don't fail the status update --->
|
|
||||||
</cfcatch>
|
|
||||||
</cftry>
|
|
||||||
</cfif>
|
|
||||||
|
|
||||||
<cfset apiAbort({
|
<cfset apiAbort({
|
||||||
"OK": true,
|
"OK": true,
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,16 @@
|
||||||
|
|
||||||
/* Other-station items (dimmed) */
|
/* Other-station items (dimmed) */
|
||||||
.line-item.other-station { opacity: 0.35; }
|
.line-item.other-station { opacity: 0.35; }
|
||||||
|
|
||||||
|
/* Station-done items */
|
||||||
|
.line-item.line-item-done { position: relative; }
|
||||||
|
.line-item.line-item-done .item-name { text-decoration: line-through; opacity: 0.5; }
|
||||||
|
.line-item.line-item-done .item-qty { opacity: 0.5; }
|
||||||
|
.line-item.line-item-done .modifiers { opacity: 0.4; }
|
||||||
|
.line-item.line-item-done::after { content: '\2713'; position: absolute; top: 10px; right: 10px; color: #4caf50; font-weight: bold; font-size: 16px; }
|
||||||
|
.btn-station-done { background: #002a10; color: #22c55e; border: 1px solid #22c55e33; }
|
||||||
|
.btn-station-done:hover { background: #003a15; }
|
||||||
|
.btn-station-done:disabled { background: #0a1a0a; color: #2a6a2a; border-color: #1a3a1a; opacity: 0.7; cursor: default; transform: none !important; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -116,6 +126,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="kds.js?v=6"></script>
|
<script src="kds.js?v=7"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
51
kds/kds.js
51
kds/kds.js
|
|
@ -366,8 +366,9 @@ function renderLineItem(item, allItems, isOtherStation = false) {
|
||||||
const modifiers = allItems.filter(mod => mod.ParentOrderLineItemID === item.OrderLineItemID);
|
const modifiers = allItems.filter(mod => mod.ParentOrderLineItemID === item.OrderLineItemID);
|
||||||
console.log(`Item: ${item.Name} (ID: ${item.OrderLineItemID}) has ${modifiers.length} direct modifiers:`, modifiers.map(m => m.Name));
|
console.log(`Item: ${item.Name} (ID: ${item.OrderLineItemID}) has ${modifiers.length} direct modifiers:`, modifiers.map(m => m.Name));
|
||||||
const otherClass = isOtherStation ? ' other-station' : '';
|
const otherClass = isOtherStation ? ' other-station' : '';
|
||||||
|
const doneClass = (item.StatusID === 1) ? ' line-item-done' : '';
|
||||||
return `
|
return `
|
||||||
<div class="line-item${otherClass}">
|
<div class="line-item${otherClass}${doneClass}">
|
||||||
<div class="line-item-main">
|
<div class="line-item-main">
|
||||||
<div class="item-name">${escapeHtml(item.Name)}</div>
|
<div class="item-name">${escapeHtml(item.Name)}</div>
|
||||||
<div class="item-qty">x${item.Quantity}</div>
|
<div class="item-qty">x${item.Quantity}</div>
|
||||||
|
|
@ -433,8 +434,36 @@ function renderAllModifiers(modifiers, allItems) {
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this station's items are all done for an order
|
||||||
|
function isStationDoneForOrder(order) {
|
||||||
|
if (!config.stationId || config.stationId === 0) return false;
|
||||||
|
const stationRootItems = order.LineItems.filter(li =>
|
||||||
|
li.ParentOrderLineItemID === 0 && (parseInt(li.StationID) || 0) === config.stationId
|
||||||
|
);
|
||||||
|
if (stationRootItems.length === 0) return true; // no items for this station
|
||||||
|
return stationRootItems.every(li => li.StatusID === 1);
|
||||||
|
}
|
||||||
|
|
||||||
// Render action buttons based on order status
|
// Render action buttons based on order status
|
||||||
function renderActionButtons(order) {
|
function renderActionButtons(order) {
|
||||||
|
const isFiltering = config.stationId && config.stationId > 0;
|
||||||
|
|
||||||
|
if (isFiltering) {
|
||||||
|
// Station worker view: per-station "Done" button
|
||||||
|
if (order.StatusID === STATUS.NEW || order.StatusID === STATUS.PREPARING) {
|
||||||
|
if (isStationDoneForOrder(order)) {
|
||||||
|
return `<button class="btn btn-station-done" disabled>Station Done</button>`;
|
||||||
|
} else {
|
||||||
|
return `<button class="btn btn-station-done" onclick="markStationDone(${order.OrderID})">Mark Station Done</button>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (order.StatusID === STATUS.READY) {
|
||||||
|
return `<button class="btn btn-complete" onclick="updateOrderStatus(${order.OrderID}, ${STATUS.COMPLETED})">Complete</button>`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manager view (no station selected): whole-order buttons
|
||||||
switch (order.StatusID) {
|
switch (order.StatusID) {
|
||||||
case STATUS.NEW:
|
case STATUS.NEW:
|
||||||
return `<button class="btn btn-start" onclick="updateOrderStatus(${order.OrderID}, ${STATUS.PREPARING})">Start Preparing</button>`;
|
return `<button class="btn btn-start" onclick="updateOrderStatus(${order.OrderID}, ${STATUS.PREPARING})">Start Preparing</button>`;
|
||||||
|
|
@ -467,6 +496,26 @@ async function updateOrderStatus(orderId, newStatusId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark station's items as done for an order
|
||||||
|
async function markStationDone(orderId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${config.apiBaseUrl}/orders/markStationDone.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ OrderID: orderId, StationID: config.stationId })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.OK) {
|
||||||
|
await loadOrders();
|
||||||
|
} else {
|
||||||
|
alert(`Failed to mark station done: ${data.MESSAGE || data.ERROR}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to mark station done:', error);
|
||||||
|
alert('Failed to mark station done. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|
||||||
// Get location label based on order type (Table vs Type)
|
// Get location label based on order type (Table vs Type)
|
||||||
|
|
|
||||||
Reference in a new issue