Merge pull request #2 from dredgy/order_screen

Order screen
This commit is contained in:
dredgy
2022-01-23 18:18:06 +10:00
committed by GitHub
32 changed files with 1608 additions and 526 deletions

View File

@@ -2,6 +2,7 @@
open DredgeFramework open DredgeFramework
open DredgePos open DredgePos
open DredgePos.Types
open Floorplan open Floorplan
open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Http
open Reservations open Reservations
@@ -41,22 +42,25 @@ let unmergeTable tableNumber =
let getFloorplanData (id: int) = let getFloorplanData (id: int) =
let tableList = Entity.getAllInVenue<floorplan_table> let tableList = Entity.GetAllInVenue<floorplan_table>
let reservationList = getReservationList tableList let reservationList = getReservationList tableList
{| {|
tables = tableList tables = tableList
decorations = Entity.getAllInVenue<floorplan_decoration> decorations = Entity.GetAllInVenue<floorplan_decoration>
activeTableNumbers = Floorplan.getActiveTables (getCurrentVenue()) activeTableNumbers = Floorplan.getActiveTables (getCurrentVenue())
rooms = Entity.getAllInVenue<floorplan_room> rooms = Entity.GetAllInVenue<floorplan_room>
reservations = reservationList reservations = reservationList
|} |}
|> ajaxSuccess |> ajaxSuccess
|> json |> json
let getOrderScreenData (id: int) = let getOrderScreenData (tableNumber: int) =
let pages = Entity.getAllInVenue<order_screen_page_group>
{| {|
order_screen_pages = pages order_screen_pages = Entity.GetAllInVenue<order_screen_page_group>
sales_categories = Entity.GetAllInVenue<sales_category>
print_groups = Entity.GetAllInVenue<print_group>
custom_item = Entity.GetAllByColumn<item> "item_code" "OPEN000" |> first
table = getTable tableNumber
|} |}
|> ajaxSuccess |> ajaxSuccess
|> json |> json
@@ -70,21 +74,18 @@ let getKeyboardLayout (language: string) =
] |> json ] |> json
let transformTable (table: floorplan_table) = let transformTable (table: floorplan_table) =
Entity.updateInDatabase table Entity.Update table
|> ajaxSuccess |> ajaxSuccess
|> json |> json
let createTable (tableData: floorplan_table) = let createTable (tableData: floorplan_table) =
let result =
if tableExists tableData.table_number = "False" then if tableExists tableData.table_number = "False" then
ajaxSuccess (addNewTable tableData) ajaxSuccess (addNewTable tableData)
else ajaxFail (tableExists tableData.table_number) else ajaxFail (tableExists tableData.table_number)
|> json
result |> json
let deleteTable (table: floorplan_table) = let deleteTable (table: floorplan_table) =
Entity.deleteById<floorplan_table> table.id Entity.DeleteById<floorplan_table> table.id
|> ignore |> ignore
table |> ajaxSuccess |> json table |> ajaxSuccess |> json
@@ -110,30 +111,30 @@ let AddDecoration (data: floorplan_decoration) =
venue_id = data.venue_id venue_id = data.venue_id
} }
Entity.addToDatabase decoration Entity.Create decoration
|> ajaxSuccess |> ajaxSuccess
|> json |> json
let UpdateDecoration (data: floorplan_decoration) = let UpdateDecoration (data: floorplan_decoration) =
Entity.updateInDatabase data Entity.Update data
|> ignore |> ignore
ajaxSuccess "true" |> json ajaxSuccess "true" |> json
let DeleteDecoration (decorationToDelete: floorplan_decoration) = let DeleteDecoration (decorationToDelete: floorplan_decoration) =
Entity.deleteById<floorplan_decoration> decorationToDelete.id Entity.DeleteById<floorplan_decoration> decorationToDelete.id
|> ajaxSuccess |> ajaxSuccess
|> json |> json
let newEmptyReservation (reservation: reservation) = let newEmptyReservation (reservation: reservation) =
let newReservation = {reservation with let newReservation = {reservation with
reservation_created_at = CurrentTime() created_at = CurrentTime()
reservation_time = CurrentTime() time = CurrentTime()
} }
if reservation.reservation_table_id > 0 then if reservation.floorplan_table_id > 0 then
let table = {(getTableById reservation.reservation_table_id) with let table = {(getTableById reservation.floorplan_table_id) with
status = 2 status = 2
default_covers = reservation.reservation_covers} default_covers = reservation.covers}
updateTablePosition table |> ignore updateTablePosition table |> ignore
let createdReservation = Floorplan.createEmptyReservation newReservation let createdReservation = Floorplan.createEmptyReservation newReservation
@@ -146,3 +147,10 @@ let unreserveTable (table: floorplan_table) =
updateTablePosition newTable |> ignore updateTablePosition newTable |> ignore
DeleteReservation newTable.id DeleteReservation newTable.id
newTable |> ajaxSuccess |> json newTable |> ajaxSuccess |> json
let loadGrid (gridId: int) =
let grid = Entity.GetById<grid> gridId
let gridHtml = OrderScreen.loadGrid gridId
if gridHtml = "Error" then ajaxFail gridHtml
else ajaxSuccess {|grid=grid;gridHtml=gridHtml|}
|> json

View File

@@ -8,7 +8,7 @@ open Dapper.FSharp
open DredgePos open DredgePos
open Types open Types
let decorationsInRoom (roomId: int) = Entity.getAllByColumn "decoration_room" roomId let decorationsInRoom (roomId: int) = Entity.GetAllByColumn "decoration_room" roomId
let getImageName (image: string, path: string) = let getImageName (image: string, path: string) =
let imageName = let imageName =

View File

@@ -38,8 +38,10 @@ let GetFileName (file: string) = Path.GetFileName file
let length (variable: 'T[]) = variable.Length let length (variable: 'T[]) = variable.Length
let first (array: 'a[]) = array[0] let first (array: 'a[]) = array[0]
let last (array: 'a[]) = array[array.Length-1] let last (array: 'a[]) = array[array.Length-1]
let filterFirst (array:'a[]) = if array.Length > 0 then [|array[0]|] else [||]
let removeFalseValues (variable: bool[]) = variable |> Array.filter id let removeFalseValues (variable: bool[]) = variable |> Array.filter id

View File

@@ -15,6 +15,7 @@
<Compile Include="Reservations.module.fs" /> <Compile Include="Reservations.module.fs" />
<Compile Include="Floorplan.module.fs" /> <Compile Include="Floorplan.module.fs" />
<Compile Include="Printer.module.fs" /> <Compile Include="Printer.module.fs" />
<Compile Include="Orders.module.fs" />
<Compile Include="OrderScreen.module.fs" /> <Compile Include="OrderScreen.module.fs" />
<Compile Include="Decorations.module.fs" /> <Compile Include="Decorations.module.fs" />
<Compile Include="Clerk.module.fs" /> <Compile Include="Clerk.module.fs" />
@@ -60,6 +61,7 @@
<PackageReference Include="SQLProvider" Version="1.2.1" /> <PackageReference Include="SQLProvider" Version="1.2.1" />
<PackageReference Include="System.Drawing.Common" Version="6.0.0-preview.5.21301.5" /> <PackageReference Include="System.Drawing.Common" Version="6.0.0-preview.5.21301.5" />
<PackageReference Include="Thoth.Json.Net" Version="5.0.0" /> <PackageReference Include="Thoth.Json.Net" Version="5.0.0" />
<PackageReference Update="FSharp.Core" Version="6.0.2-beta.21631.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,6 +1,7 @@
module Floorplan module Floorplan
open DredgePos open DredgePos
open DredgePos.Types
open Org.BouncyCastle.Asn1.X509 open Org.BouncyCastle.Asn1.X509
open Reservations open Reservations
@@ -116,16 +117,16 @@ let getRoom (roomId: int) =
where (eq "id" roomId) where (eq "id" roomId)
} |> db.Select<floorplan_room> |> first } |> db.Select<floorplan_room> |> first
let updateTablePosition (floorplanTable: floorplan_table) = Entity.updateInDatabase floorplanTable let updateTablePosition (floorplanTable: floorplan_table) = Entity.Update floorplanTable
let createEmptyReservation (reservation: reservation) = let createEmptyReservation (reservation: reservation) =
update { update {
table "floorplan_tables" table "floorplan_tables"
set {| status = 2 |} set {| status = 2 |}
where(eq "id" reservation.reservation_table_id) where(eq "id" reservation.floorplan_table_id)
} |> db.Update |> ignore } |> db.Update |> ignore
Entity.addToDatabase reservation Entity.Create reservation
let getChildTables tableNumber = let getChildTables tableNumber =
let table = getTable tableNumber let table = getTable tableNumber
@@ -165,7 +166,7 @@ let tableExists (tableNumber: int) =
match numberOfResults with match numberOfResults with
| 0 -> | 0 ->
let allTables = let allTables =
Entity.getAllInVenue<floorplan_table> Entity.GetAllInVenue<floorplan_table>
|> Array.map(findChildTable tableNumber) |> Array.map(findChildTable tableNumber)
|> Array.filter(fun tableNumber -> tableNumber <> 0) |> Array.filter(fun tableNumber -> tableNumber <> 0)
@@ -192,7 +193,7 @@ let addNewTableWithoutOutput (newTable: floorplan_table) =
} }
|> db.Insert |> db.Insert
let addNewTable (newTable: floorplan_table) = Entity.addToDatabase newTable let addNewTable (newTable: floorplan_table) = Entity.Create newTable
let mergeTables parent child = //Merge two tables together let mergeTables parent child = //Merge two tables together
if parent = child then false else if parent = child then false else
@@ -246,7 +247,7 @@ let mergeTables parent child = //Merge two tables together
where (eq "table_number" parent + eq "venue_id" (getCurrentVenue())) where (eq "table_number" parent + eq "venue_id" (getCurrentVenue()))
} |> db.Update |> ignore } |> db.Update |> ignore
Entity.deleteById<floorplan_table> newChildTable.id Entity.DeleteById<floorplan_table> newChildTable.id
|> ignore |> ignore
true true
@@ -289,30 +290,19 @@ let makeRoomButton (room: floorplan_room) =
Theme.loadTemplateWithVars "roomButton" vars Theme.loadTemplateWithVars "roomButton" vars
let getReservationList (tableList: floorplan_table[]) = let getReservationList (tableList: floorplan_table[]) =
let tableIds = tableList |> Array.collect Entity.GetAllRelated<reservation, floorplan_table>
tableList
|> Array.map(fun table -> box table.id)
|> List.ofArray
if tableIds.Length > 0 then
select {
table "reservations"
where (isIn "reservation_table_id" tableIds)
}
|> db.Select<reservation>
else [||]
let newReservation name time covers = let newReservation name time covers =
let reservation = { let reservation = {
id = 0 id = 0
reservation_name = name name = name
reservation_time = time time = time
reservation_covers = covers covers = covers
reservation_table_id = 0 floorplan_table_id = 0
reservation_created_at = CurrentTime() created_at = CurrentTime()
} }
Entity.addToDatabase reservation Entity.Create reservation
let tableList () = Entity.getAllInVenue<floorplan_table> let tableList () = Entity.GetAllInVenue<floorplan_table>

View File

@@ -2,13 +2,14 @@
open Dapper.FSharp open Dapper.FSharp
open DredgeFramework open DredgeFramework
open Pluralize.NET.Core open Pluralize.NET.Core
open FSharp.Reflection
let getDatabaseTable<'x> = let GetDatabaseTable<'x> =
let typeName = typeof<'x>.Name let typeName = typeof<'x>.Name
Pluralizer().Pluralize typeName Pluralizer().Pluralize typeName
let addToDatabase (record: 'x)= let Create (record: 'x)=
let tableName = getDatabaseTable<'x> let tableName = GetDatabaseTable<'x>
insert { insert {
table tableName table tableName
value record value record
@@ -18,42 +19,50 @@ let addToDatabase (record: 'x)=
|> first |> first
let inline updateInDatabase (record: ^x) = let inline Update (record: ^x) =
let tableName = getDatabaseTable<'x> let tableName = GetDatabaseTable<'x>
let id = ((^x) : (member id : int) (record)) let id = ((^x) : (member id : int) record)
update { update {
table tableName table tableName
set record set record
where (eq "id" id) where (eq "id" id)
excludeColumn "id"
} }
|> db.Update |> db.Update
let getAll<'x> = let GetAll<'x> =
let typeName = typeof<'x>.Name let tableName = GetDatabaseTable<'x>
let tableName = Pluralizer().Pluralize typeName
select { select {
table tableName table tableName
} }
|> db.Select<'x> |> db.Select<'x>
let getAllByColumn<'x> (column: string) (value: obj) = let GetAllByColumn<'x> (column: string) (value: obj) =
let typeName = typeof<'x>.Name let tableName = GetDatabaseTable<'x>
let tableName = Pluralizer().Pluralize typeName
select { select {
table tableName table tableName
where (eq column value) where (eq column value)
} |> db.Select<'x> } |> db.Select<'x>
let getAllInVenue<'x> = getAllByColumn<'x> "venue_id" (getCurrentVenue ()) let GetAllInVenue<'x> = GetAllByColumn<'x> "venue_id" (getCurrentVenue ())
let getById<'x> (id: int) = getAllByColumn<'x> "id" id |> first let GetById<'x> (id: int) = GetAllByColumn<'x> "id" id |> first
let deleteById<'x> id = let inline GetRelated<'x, .. > (entity: ^y) =
let columnName = typeof<'x>.Name + "_id"
let primaryKeyValue = typeof<'y>.GetProperty(columnName).GetValue(entity) :?> int
GetById<'x> primaryKeyValue
let inline GetAllRelated<'x, .. > (entity: ^y) =
let id = typeof<'y>.GetProperty("id").GetValue(entity) :?> int
let columnName = typeof<'y>.Name + "_id"
GetAllByColumn<'x> columnName id
let DeleteById<'x> id =
let typeName = typeof<'x>.Name let typeName = typeof<'x>.Name
let tableName = Pluralizer().Pluralize typeName let tableName = Pluralizer().Pluralize typeName
let entity = GetById<'x> id
let entity = getById<'x> id
delete { delete {
table tableName table tableName
@@ -61,3 +70,7 @@ let deleteById<'x> id =
} |> db.Delete |> ignore } |> db.Delete |> ignore
entity entity
let inline Delete< ^x when ^x: (member id: int) > (entity: ^x) =
typeof<'x>.GetProperty("id").GetValue(entity) :?> int
|> DeleteById<'x>

View File

@@ -23,11 +23,11 @@ let get var =
else else
"Missing language variable: " + var "Missing language variable: " + var
let getAndReplace languageVar replacements = let getAndReplace languageVar (replacements: 'x list) =
let langString = get languageVar let langString = get languageVar
replacements replacements
|> List.mapi (fun index string |> List.mapi (fun index replacement
-> index + 1, string) -> index + 1, replacement.ToString())
|> List.fold (fun (result: string) (index, string) |> List.fold (fun (result: string) (index, string)
-> result.Replace($"[{index}]", string) -> result.Replace($"[{index}]", string)
) langString ) langString

View File

@@ -1,34 +1,27 @@
module OrderScreen module OrderScreen
open System.Security.Cryptography.Xml
open DredgeFramework open DredgeFramework
open DredgePos open DredgePos
open DredgePos.Types
open FSharp.Collections open FSharp.Collections
open Thoth.Json.Net open Thoth.Json.Net
open Types
open Theme open Theme
let htmlAttributes (attributes: Map<string, string>) =
" " + (attributes
|> Map.toArray
|> Array.map (fun (attribute, value) -> attribute+"="+value)
|> String.concat " ")
let getAllPageGrids () = Entity.GetAllInVenue<order_screen_page_group>
let getAllPageGrids () = Entity.getAllInVenue<order_screen_page_group>
|> Array.filter(fun pageGroup -> pageGroup.grid_id <> 0) |> Array.filter(fun pageGroup -> pageGroup.grid_id <> 0)
|> Array.map(fun pageGroup -> (Entity.getById<grid> pageGroup.grid_id), pageGroup) |> Array.map(fun pageGroup -> (Entity.GetById<grid> pageGroup.grid_id), pageGroup)
let getImageButtonData (button: button) = let getImageButtonData (button: button) =
let item = Entity.getAllByColumn<item> "item_code" button.primary_action_value let itemCode =
if button.primary_action = "item" then button.primary_action_value
else button.secondary_action_value
let item = Entity.GetAllByColumn<item> "item_code" itemCode
|> first |> first
let extraData = let extraData =
map [ map [
"data-item-code", item.item_code "data-item", jsonEncode item
"data-item-price", item.price1.ToString()
"data-item-name", item.item_name
"data-item-type", item.item_type
"data-item-category", item.item_category.ToString()
] |> htmlAttributes ] |> htmlAttributes
{| {|
@@ -36,9 +29,31 @@ let getImageButtonData (button: button) =
text = item.item_name text = item.item_name
|} |}
let getGridButtonData (button: button) =
let gridId =
if button.primary_action = "grid" then button.primary_action_value
else button.secondary_action_value
|> int
let grid = Entity.GetById<grid> gridId
{|
extra_data = map ["data-grid", jsonEncode gridId] |> htmlAttributes
text = grid.grid_name
|}
let getActionData (button: button) (action: string) =
let actionValue =
if action = "primary" then button.primary_action
else button.secondary_action
match actionValue with
| "item" -> getImageButtonData button
| "grid" -> getGridButtonData button
| "spacer" -> {|extra_data=""; text=""|}
| _ -> {|extra_data=""; text=""|}
let renderButton (buttonId: int) = let renderButton (buttonId: int) =
let button = Entity.getById<button> buttonId let button = Entity.GetById<button> buttonId
let extra_styles = let extra_styles =
match button.extra_styles.Length with match button.extra_styles.Length with
@@ -50,55 +65,73 @@ let renderButton (buttonId: int) =
then "invisible" then "invisible"
else "" else ""
let image = if button.image.Length > 0 let image = if button.image.Length > 0 then loadTemplateWithVars "orderScreen/button_image" (map ["image", button.image]) else ""
then loadTemplateWithVars "orderScreen/button_image" (map ["image", button.image])
else ""
let extraClasses = [|imageClass; spacerClass|] |> String.concat " " let extraClasses = [|imageClass; spacerClass|] |> String.concat " "
let action_data = let primary_action_data = getActionData button "primary"
match button.primary_action with let secondary_action_data = getActionData button "secondary"
| "item" -> getImageButtonData button
| "spacer" -> {|extra_data=""; text=""|} let action_extra_data = primary_action_data.extra_data + " " + secondary_action_data.extra_data
| _ -> {|extra_data=""; text=""|} let button_text =
if button.text.Length > 0 then button.text
else
if primary_action_data.text.Length > 0 then primary_action_data.text
else secondary_action_data.text
let vars = map [ let vars = map [
"extra_classes", button.extra_classes + " " + extraClasses "extra_classes", button.extra_classes + " " + extraClasses
"extra_styles", extra_styles "extra_styles", extra_styles
"primary_action", button.primary_action "primary_action", button.primary_action
"secondary_action", button.secondary_action "secondary_action", button.secondary_action
"text", if button.text.Length >0 then button.text else action_data.text "text", button_text
"image", image "image", image
"extra_data", action_data.extra_data "extra_data", action_extra_data
] ]
loadTemplateWithVars "orderScreen/grid_button" vars loadTemplateWithVars "orderScreen/grid_button" vars
let renderPage (buttonHTML: string) = let renderPage (grid: grid) (buttonHTML: string) =
let vars = map [ let vars = map ["pageButtons", buttonHTML; "rows", string grid.grid_rows; "cols", string grid.grid_cols]
"pageButtons", buttonHTML
]
loadTemplateWithVars "orderScreen/page" vars loadTemplateWithVars "orderScreen/page" vars
let renderPageGroup (pageGroup: order_screen_page_group) (pageHTML: string) = let renderPageGroup (pageGroup: order_screen_page_group) (pageHTML: string) =
let vars = map [ let vars = map [
"pages", pageHTML "pages", pageHTML
"page_group_id", pageGroup.id.ToString() "page_group_id", string pageGroup.id
] ]
loadTemplateWithVars "orderScreen/page_group" vars loadTemplateWithVars "orderScreen/page_group" vars
let getPagesHTML (gridInfo: grid * order_screen_page_group) = let printGroupPosButton (printGroup: print_group) =
let grid, pageGroup = gridInfo PosButton (language.getAndReplace "print_with" [printGroup.name]) "printGroupOverrideButton toggle" $"""data-value="{printGroup.id}" """
let pages = grid.grid_data |> Decode.Auto.fromString<Map<string, int[]>> let generateSalesCategoryOverrideButtons () =
Entity.GetAllInVenue<print_group>
|> Array.map printGroupPosButton
|> Array.append [|PosButton (language.getAndReplace "print_with" ["default"]) "printGroupOverrideButton toggle default active" """data-value="0" """|]
|> String.concat "\n"
match pages with
let renderGrid (grid: grid) =
let gridData = grid.grid_data |> Decode.Auto.fromString<Map<string, int[]>>
match gridData with
| Error _ -> "Error" | Error _ -> "Error"
| Ok pages -> | Ok pages ->
pages pages
|> Map.toArray |> Map.toArray
|> Array.map snd |> Array.map snd
|> Array.map(fun row -> row |> Array.map renderButton |> String.concat "\n") |> Array.map(fun row -> row |> Array.map renderButton |> String.concat "\n")
|> Array.map renderPage |> Array.map (renderPage grid)
|> String.concat "\n" |> String.concat "\n"
let loadGrid gridId = renderGrid (Entity.GetById<grid> gridId)
let getPagesHTML (gridInfo: grid * order_screen_page_group) =
let grid, pageGroup = gridInfo
renderGrid grid
|> renderPageGroup pageGroup |> renderPageGroup pageGroup

3
Orders.module.fs Normal file
View File

@@ -0,0 +1,3 @@
module Orders
let getHighestOrderNumber () = 6

View File

@@ -2,6 +2,7 @@
open System open System
open DredgePos.Types open DredgePos.Types
open FSharp.Data
open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Http
open Floorplan open Floorplan
open Giraffe open Giraffe
@@ -19,7 +20,7 @@ let loadFloorplan (ctx: HttpContext) : HttpHandler =
Session.RequireClerkAuthentication ctx Session.RequireClerkAuthentication ctx
let roomMenu = let roomMenu =
Entity.getAllInVenue<floorplan_room> Entity.GetAllInVenue<floorplan_room>
|> Array.map makeRoomButton |> Array.map makeRoomButton
|> String.concat "\n" |> String.concat "\n"
@@ -36,12 +37,28 @@ let loadFloorplan (ctx: HttpContext) : HttpHandler =
htmlString <| Theme.loadTemplateWithVarsArraysScriptsAndStyles "floorplan" variables arrays scripts styles htmlString <| Theme.loadTemplateWithVarsArraysScriptsAndStyles "floorplan" variables arrays scripts styles
let loadOrderScreen (ctx: HttpContext) : HttpHandler = let loadOrderScreen (ctx: HttpContext) (tableNumber: int) : HttpHandler =
Session.RequireClerkAuthentication ctx Session.RequireClerkAuthentication ctx
let covers = if tableNumber > 0 then (getTable tableNumber).default_covers else 0
let coverString = language.getAndReplace "covers" [covers]
let changeCoverNumberButton = if tableNumber > 0 then Theme.loadTemplateWithVars "orderScreen/change_cover_number_button" (map ["covers", coverString]) else ""
let orderNumber =
if tableNumber > 0 then language.getAndReplace "active_table" [tableNumber]
else language.get "new_order"
let containerAttributes =
if tableNumber > 0 then
map ["data-table", jsonEncode (getTable tableNumber)]
|> Theme.htmlAttributes
else ""
let categoryList = let categoryList =
Entity.getAll<order_screen_page_group> Entity.GetAllInVenue<order_screen_page_group>
|> Array.filter (fun category -> category.id <> 0) |> Array.filter (fun page_group -> page_group.id <> 0)
|> Array.sortBy (fun {order=order} -> order)
|> Array.map (fun category -> |> Array.map (fun category ->
let categoryMap = recordToMap category let categoryMap = recordToMap category
let categoryArray = map ["page", categoryMap] let categoryArray = map ["page", categoryMap]
@@ -54,19 +71,33 @@ let loadOrderScreen (ctx: HttpContext) : HttpHandler =
|> Array.map OrderScreen.getPagesHTML |> Array.map OrderScreen.getPagesHTML
|> String.concat "\n" |> String.concat "\n"
let coverSelectorButtons =
Array.init (covers+1) id
|> Array.map(fun coverNumber ->
let text = if coverNumber > 0 then language.getAndReplace "selected_cover" [coverNumber]
else language.get "cover_zero"
Theme.PosButton text "coverSelectorButton" $"""data-cover="{coverNumber}" """)
|> String.concat "\n"
let variables = map [ let variables = map [
"title", "Order" "title", "Order"
"containerAttributes", containerAttributes
"categoryList", categoryList "categoryList", categoryList
"pageGroups", grids "pageGroups", grids
"orderNumber", orderNumber
"changeCoverNumberButton", changeCoverNumberButton
"covers", coverString
"salesCategoryOverrideButtons", OrderScreen.generateSalesCategoryOverrideButtons ()
"coverSelectorButtons", coverSelectorButtons
] ]
let styles = ["dredgepos.orderScreen.css"] let styles = ["dredgepos.orderScreen.css"]
let scripts = ["dredgepos.orderScreen.js"] let scripts = ["dredgepos.tables.js";"../external/currency.min.js";"dredgepos.orderScreen.js"; ]
let currentClerk = recordToMap <| Session.getCurrentClerk ctx let currentClerk = recordToMap <| Session.getCurrentClerk ctx
let arrays = map ["clerk", currentClerk] let arrays = map ["clerk", currentClerk]
htmlString <| Theme.loadTemplateWithVarsArraysScriptsAndStyles "orderScreen" variables arrays scripts styles Theme.loadTemplateWithVarsArraysScriptsAndStyles "orderScreen" variables arrays scripts styles
|> htmlString
let getOpenTables() = let getOpenTables() =
let rows = openTables() let rows = openTables()

View File

@@ -2,6 +2,7 @@ namespace WebApplication
open DredgePos open DredgePos
open DredgePos.Types
open Microsoft.AspNetCore.Server.Kestrel.Core open Microsoft.AspNetCore.Server.Kestrel.Core
open Reservations open Reservations
open Saturn open Saturn
@@ -40,10 +41,11 @@ module Program =
getf "/unmergeTable/%i" AjaxController.unmergeTable getf "/unmergeTable/%i" AjaxController.unmergeTable
getf "/tableExists/%i" (fun tableNumber -> json <| Floorplan.tableExists tableNumber) getf "/tableExists/%i" (fun tableNumber -> json <| Floorplan.tableExists tableNumber)
} }
let orderScreenRouter = router { let orderScreenRouter = router {
pipe_through browser pipe_through browser
getf "/getOrderScreenData/%i" AjaxController.getOrderScreenData getf "/getOrderScreenData/%i" AjaxController.getOrderScreenData
getf "/getGridHtml/%i" AjaxController.loadGrid
post "/updateCovers" (bindJson<floorplan_table> (fun table -> Entity.Update table |> Array.head |> DredgeFramework.ajaxSuccess |> json))
} }
let pageRouter = router { let pageRouter = router {
@@ -52,7 +54,8 @@ module Program =
get "/" (redirectTo true "/login") get "/" (redirectTo true "/login")
get "/login" (warbler (fun _ -> PageController.loadHomePage() )) get "/login" (warbler (fun _ -> PageController.loadHomePage() ))
get "/floorplan" (warbler (fun ctx -> PageController.loadFloorplan (snd ctx))) get "/floorplan" (warbler (fun ctx -> PageController.loadFloorplan (snd ctx)))
get "/order" (warbler (fun ctx -> PageController.loadOrderScreen (snd ctx))) get "/order" (warbler (fun ctx -> PageController.loadOrderScreen (snd ctx) 0))
getf "/order/%i" (fun number -> (warbler (fun ctx -> PageController.loadOrderScreen (snd ctx) number)))
forward "/ajax" floorplanRouter forward "/ajax" floorplanRouter
forward "/orderScreen" orderScreenRouter forward "/orderScreen" orderScreenRouter
} }

View File

@@ -1,6 +1,6 @@
module Theme module Theme
open System open System.Web
open System.IO open System.IO
open System.Collections.Generic open System.Collections.Generic
open System.Text.RegularExpressions open System.Text.RegularExpressions
@@ -49,7 +49,7 @@ let ParseVariables (varArray: Map<string, string>) (html:string) =
if varName |> ToLowerCase = "title" then titlePrefix varArray[varName] if varName |> ToLowerCase = "title" then titlePrefix varArray[varName]
else varArray[varName] else varArray[varName]
else else
"<!--[Undefined Variable: " + varName + "]-->" ""
)) ))
let ParseArrays (arrayArray: Map<string, Map<string, string>>) (string:string) = let ParseArrays (arrayArray: Map<string, Map<string, string>>) (string:string) =
@@ -78,7 +78,7 @@ let ParseSimpleLanguageVariables (string:string) =
let ParseLanguageVariablesWithReplacements (string: string) = let ParseLanguageVariablesWithReplacements (string: string) =
Regex.Replace(string, "<!--\[lang\:(.*?)\|(.*?)\]-->", Regex.Replace(string, "<!--\[lang\:(.*?)\|(.*?)\]-->",
new MatchEvaluator( MatchEvaluator(
fun matchedVar -> fun matchedVar ->
let varName = matchedVar.Groups[1].ToString() let varName = matchedVar.Groups[1].ToString()
let replacements = matchedVar.Groups[2].ToString() let replacements = matchedVar.Groups[2].ToString()
@@ -138,3 +138,18 @@ let loadTemplateWithVarsAndStyles = loadTemplateWithVarsAndScripts
let loadTemplateWithVarsScriptsAndStyles templateName vars scripts styles = let loadTemplateWithVarsScriptsAndStyles templateName vars scripts styles =
loadTemplateWithVarsArraysScriptsAndStyles templateName vars Map.empty<string, Map<string, string>> scripts styles loadTemplateWithVarsArraysScriptsAndStyles templateName vars Map.empty<string, Map<string, string>> scripts styles
let htmlAttributes (attributes: Map<string, string>) =
" " + (attributes
|> Map.toArray
|> Array.map (fun (attribute, value) -> attribute+"='"+HttpUtility.HtmlEncode value + "'")
|> String.concat " ")
let PosButton (text: string) (classes: string) (attributes: string) =
let vars = map [
"text", text
"classes", classes
"attributes", attributes
]
loadTemplateWithVars "components/posButton" vars

View File

@@ -3,11 +3,17 @@ module DredgePos.Types
[<CLIMutable>] [<CLIMutable>]
type reservation = { type reservation = {
id: int id: int
reservation_name: string name: string
reservation_time: int time: int
reservation_covers: int covers: int
reservation_table_id: int floorplan_table_id: int
reservation_created_at: int created_at: int
}
[<CLIMutable>]
type venue = {
id: int
venue_name: string
} }
[<CLIMutable>] [<CLIMutable>]
@@ -29,11 +35,20 @@ type floorplan_table = {
} }
[<CLIMutable>] [<CLIMutable>]
type category = { type print_group = {
id: int id: int
category_name: string name: string
category_print_group: string printer: int
category_department: string venue_id: int
}
[<CLIMutable>]
type sales_category = {
id: int
parent: int
name: string
print_group: int
venue_id: int
} }
[<CLIMutable>] [<CLIMutable>]
@@ -64,7 +79,7 @@ type clerk = {id: int; clerk_name: string; clerk_login_code: int; clerk_usergrou
type session = {id: int; session_id: string; clerk_json: string; clerk_id: int; expires: int} type session = {id: int; session_id: string; clerk_json: string; clerk_id: int; expires: int}
[<CLIMutable>] [<CLIMutable>]
type order_screen_page_group = {id: int; venue_id: int; label: string; grid_id: int} type order_screen_page_group = {id: int; order: int; venue_id: int; label: string; grid_id: int}
[<CLIMutable>] [<CLIMutable>]
type grid = {id: int; grid_name: string; grid_rows: int; grid_cols: int; grid_data: string} type grid = {id: int; grid_name: string; grid_rows: int; grid_cols: int; grid_data: string}
@@ -89,9 +104,9 @@ type item = {
item_category: int item_category: int
item_name: string item_name: string
item_type: string item_type: string
price1: float price1: int
price2: float price2: int
price3: float price3: int
price4: float price4: int
price5: float price5: int
} }

View File

@@ -35,10 +35,12 @@
"functions_header":"Functions", "functions_header":"Functions",
"close_order_function":"Close Without Saving", "close_order_function":"Close Without Saving",
"accumulate_function":"Accumulate Items", "accumulate_function":"Accumulate Items",
"multiplier":"Enter a Number",
"void":"Void", "void":"Void",
"pay_function":"Pay", "pay_function":"Pay",
"print_function":"Save & Print", "print_function":"Save & Print",
"freetext_button":"Keyboard", "freetext_button":"Custom Instruction",
"custom_item_button":"Custom Item",
"numpad_button":"Numpad", "numpad_button":"Numpad",
"select_covers":"Cover #", "select_covers":"Cover #",
"how_many_covers":"How many people are on this table?", "how_many_covers":"How many people are on this table?",
@@ -51,6 +53,8 @@
"merge_table":"Merge Table", "merge_table":"Merge Table",
"unmerge_table":"Unmerge Table", "unmerge_table":"Unmerge Table",
"order_table":"Place Order", "order_table":"Place Order",
"order_number": "Order [1]",
"new_order": "New Order",
"view_table":"View Table", "view_table":"View Table",
"reserve_table":"Reserve Table", "reserve_table":"Reserve Table",
"unreserve_table":"Delete Reservation", "unreserve_table":"Delete Reservation",
@@ -85,5 +89,9 @@
"partial_table":"Partial Table", "partial_table":"Partial Table",
"paying_table":"Paying Table [1]", "paying_table":"Paying Table [1]",
"msgbox_amount_select":"Enter Amount", "msgbox_amount_select":"Enter Amount",
"msgbox_qty_select":"How many would you like to select?" "msgbox_qty_select":"How many would you like to select?",
"void_mode": "Void Mode is ON.",
"enter_item_name": "Enter Item Name",
"enter_item_price": "Enter Item Price"
} }

View File

@@ -0,0 +1,12 @@
/*
currency.js - v2.0.4
http://scurker.github.io/currency.js
Copyright (c) 2021 Jason Wilson
Released under MIT license
*/
(function(e,g){"object"===typeof exports&&"undefined"!==typeof module?module.exports=g():"function"===typeof define&&define.amd?define(g):(e=e||self,e.currency=g())})(this,function(){function e(b,a){if(!(this instanceof e))return new e(b,a);a=Object.assign({},m,a);var d=Math.pow(10,a.precision);this.intValue=b=g(b,a);this.value=b/d;a.increment=a.increment||1/d;a.groups=a.useVedic?n:p;this.s=a;this.p=d}function g(b,a){var d=2<arguments.length&&void 0!==arguments[2]?arguments[2]:!0;var c=a.decimal;
var h=a.errorOnInvalid,k=a.fromCents,l=Math.pow(10,a.precision),f=b instanceof e;if(f&&k)return b.intValue;if("number"===typeof b||f)c=f?b.value:b;else if("string"===typeof b)h=new RegExp("[^-\\d"+c+"]","g"),c=new RegExp("\\"+c,"g"),c=(c=b.replace(/\((.*)\)/,"-$1").replace(h,"").replace(c,"."))||0;else{if(h)throw Error("Invalid Input");c=0}k||(c=(c*l).toFixed(4));return d?Math.round(c):c}var m={symbol:"$",separator:",",decimal:".",errorOnInvalid:!1,precision:2,pattern:"!#",negativePattern:"-!#",format:function(b,
a){var d=a.pattern,c=a.negativePattern,h=a.symbol,k=a.separator,l=a.decimal;a=a.groups;var f=(""+b).replace(/^-/,"").split("."),q=f[0];f=f[1];return(0<=b.value?d:c).replace("!",h).replace("#",q.replace(a,"$1"+k)+(f?l+f:""))},fromCents:!1},p=/(\d)(?=(\d{3})+\b)/g,n=/(\d)(?=(\d\d)+\d\b)/g;e.prototype={add:function(b){var a=this.s,d=this.p;return e((this.intValue+g(b,a))/(a.fromCents?1:d),a)},subtract:function(b){var a=this.s,d=this.p;return e((this.intValue-g(b,a))/(a.fromCents?1:d),a)},multiply:function(b){var a=
this.s;return e(this.intValue*b/(a.fromCents?1:Math.pow(10,a.precision)),a)},divide:function(b){var a=this.s;return e(this.intValue/g(b,a,!1),a)},distribute:function(b){var a=this.intValue,d=this.p,c=this.s,h=[],k=Math[0<=a?"floor":"ceil"](a/b),l=Math.abs(a-k*b);for(d=c.fromCents?1:d;0!==b;b--){var f=e(k/d,c);0<l--&&(f=f[0<=a?"add":"subtract"](1/d));h.push(f)}return h},dollars:function(){return~~this.value},cents:function(){return~~(this.intValue%this.p)},format:function(b){var a=this.s;return"function"===
typeof b?b(this,a):a.format(this,Object.assign({},a,b))},toString:function(){var b=this.s,a=b.increment;return(Math.round(this.intValue/this.p/a)*a).toFixed(b.precision)},toJSON:function(){return this.value}};return e});

View File

@@ -1,4 +1,6 @@
let Application : ApplicationState = { /// <reference path="./typings/currency.d.ts" />
const Application: ApplicationState = {
keyboard: null, keyboard: null,
mode: [], mode: [],
languageVars: {} languageVars: {}
@@ -6,14 +8,14 @@
/** Parses a language variable. */ /** Parses a language variable. */
let lang = (key: string, replacements?: string[] | string) => { const lang = (key: string, replacements?: string[] | string) => {
let finalValue = Application.languageVars[key] || '' let finalValue = Application.languageVars[key] || ''
if (!replacements) return finalValue if (!replacements) return finalValue
if (typeof replacements === 'string') replacements = [replacements] if (typeof replacements === 'string') replacements = [replacements]
replacements.forEach((replacement, index) => { replacements.forEach((replacement, index) => {
let correctIndex = index+1 const correctIndex = index + 1
finalValue = finalValue.replace(`[${correctIndex}]`, replacement) finalValue = finalValue.replace(`[${correctIndex}]`, replacement)
}) })
@@ -21,12 +23,12 @@
} }
/** Check if a variable is defined */ /** Check if a variable is defined */
let defined = (variable: any) => { const defined = (variable: any) => {
return typeof variable !== 'undefined' return typeof variable !== 'undefined'
} }
/** Call an Ajax function asynchronously */ /** Call an Ajax function asynchronously */
let ajax = (endpoint : string, data: any, method = 'POST', successFunction : Function , errorFunction : Function, beforeFunction: any) => { const ajax = (endpoint: string, data: any, method = 'POST', successFunction: Function, errorFunction: Function, beforeFunction: any) => {
data = (data == null) ? data : JSON.stringify(data) data = (data == null) ? data : JSON.stringify(data)
return $.ajax({ return $.ajax({
url: endpoint, url: endpoint,
@@ -43,14 +45,12 @@
beforeSend: beforeFunction beforeSend: beforeFunction
}) })
} }
/* /*
For the flow of the app, synchronous is commonly preferred For the flow of the app, synchronous is commonly preferred
though trying to keep its usage as low as possible. though trying to keep its usage as low as possible.
*/ */
let ajaxSync = (endpoint : string, data?: any, method = 'POST') => { const ajaxSync = (endpoint: string, data?: any, method = 'POST') => {
let response = JSON.parse( const response = JSON.parse(
$.ajax({ $.ajax({
url: endpoint, url: endpoint,
method: method, method: method,
@@ -67,18 +67,17 @@
} }
/* Redirect to a specific URL */ /* Redirect to a specific URL */
let redirect = (url: string) : void => location.assign(url) const redirect = (url: string): void => location.assign(url)
const resize = () => { const resize = () => {
$('#pageContainer').height(window.innerHeight + "px"); $('#pageContainer').height(window.innerHeight + "px");
} }
let setupCore = (languageVars: Record<string, string>) => { const setupCore = (languageVars: Record<string, string>) => {
Application.languageVars = languageVars Application.languageVars = languageVars
const doc = $(document) const doc = $(document)
doc.on('click', '#alertNo, #alertOk', hideAlerts) doc.on('click', '#alertNo, #alertOk', hideAlerts)
doc.on('click', '.toggle', toggle)
window.addEventListener('resize', resize) window.addEventListener('resize', resize)
resize() resize()
@@ -86,9 +85,8 @@
} }
// @ts-ignore const posAlert = (message: string, title = 'Message') => {
let posAlert = (message: string, title='Message') => { const alertBox = $('#alert')
let alertBox = $('#alert')
alertBox.css('display', 'flex'); alertBox.css('display', 'flex');
alertBox.data('value', ''); alertBox.data('value', '');
$('#alertHeading').text(title); $('#alertHeading').text(title);
@@ -99,8 +97,8 @@
$('#alertNo').css('display', 'none'); $('#alertNo').css('display', 'none');
} }
let confirmation = (message: string, data: any, title='Confirm', submitFunction = (data: any) => {hideAlerts()}) => { const confirmation = (message: string, data: any, title = 'Confirm', submitFunction = (data: any) => {hideAlerts()}) => {
let alert = $('#alert') const alert = $('#alert')
$(document).on('click', '#alert #alertYes', () => { $(document).on('click', '#alert #alertYes', () => {
hideAlerts() hideAlerts()
@@ -118,38 +116,41 @@
} }
let hideAlerts = () => $('#alert').hide() const hideAlerts = () => $('#alert').hide()
let turnOnMode = (mode : PosMode) => { const turnOnMode = (mode: PosMode) => {
Application.mode.push(mode) Application.mode.push(mode)
setElementVisibilityByMode() setElementVisibilityByMode()
} }
let turnOffMode = (mode : PosMode) => { const turnOffMode = (mode: PosMode) => {
Application.mode = Application.mode.filter((value) => value != mode) Application.mode = Application.mode.filter((value) => value != mode)
setElementVisibilityByMode() setElementVisibilityByMode()
} }
let toggleMode = (mode: PosMode) => { const toggleMode = (mode: PosMode) => {
if (!isInMode(mode)) if (!isInMode(mode))
turnOnMode(mode) turnOnMode(mode)
else else
turnOffMode(mode) turnOffMode(mode)
} }
let clearModes = () => {Application.mode = []} const clearModes = () => {
let isInMode = (mode: PosMode) => Application.mode.includes(mode) Application.mode = []
setElementVisibilityByMode()
}
let setElementVisibilityByMode = () => { const isInMode = (mode: PosMode) => Application.mode.includes(mode)
const setElementVisibilityByMode = () => {
const mode = Application.mode const mode = Application.mode
const elements = $('[data-visible-in-mode]') const elements = $('[data-visible-in-mode]')
elements.each((index, elem) => { elements.each((index, elem) => {
let element = $(elem) const element = $(elem)
let visibleInModes : PosModes = element.data('visible-in-mode') const visibleInModes: PosModes = element.data('visible-in-mode')
let showElement = visibleInModes.every( visibleMode => { const showElement = visibleInModes.every(visibleMode => {
return mode.includes(visibleMode) return mode.includes(visibleMode)
}); });
@@ -162,9 +163,9 @@
const invisibleElements = $('[data-invisible-in-mode]') const invisibleElements = $('[data-invisible-in-mode]')
invisibleElements.each((index, elem) => { invisibleElements.each((index, elem) => {
let element = $(elem) const element = $(elem)
let inVisibleInModes: PosModes = element.data('invisible-in-mode') const inVisibleInModes: PosModes = element.data('invisible-in-mode')
let hideElement = inVisibleInModes.some(invisibleMode => { const hideElement = inVisibleInModes.some(invisibleMode => {
return mode.includes(invisibleMode) return mode.includes(invisibleMode)
}) })
element.toggle(!hideElement) element.toggle(!hideElement)
@@ -183,4 +184,56 @@
} }
const pulseElement = (element: JQuery) => element.addClass('pulse').on('animationend', () => element.removeClass('pulse'))
Array.prototype.where = function<x>(this: x[], property: string, value: any) {
return this.filter( item => (item as any)[property] === value)[0] || null
}
const money = (amount: number, fromCents=true) => currency(amount, {fromCents: fromCents})
const moneyFromString = (amount: string) => currency(amount)
const toggle = (e: JQuery.TriggeredEvent) => {
const button = $(e.target)
const toggleGroup = button.closest('.toggleGroup')
const input = toggleGroup.find('input.value')
const isActive = button.hasClass('active')
toggleGroup
.find('.toggle')
.removeClass('active')
let value = !isActive
? button.addClass('active').data('value')
: toggleGroup
.find('.toggle.default')
.addClass('active')
.data('value')
input.val(value).trigger('change')
}
const resetToggle = (input: JQuery) => {
input
.closest('.toggleGroup')
.find('.toggle.default')
.trigger('click')
}
//Id generator.
function* newestId(){
let id = 0
while(true){
id++
yield id
}
}
const loadTemplate = (templateSelector: string) => {
const content = $(templateSelector)
.clone()
.removeAttr('id')
.prop('content')
return $(content)
}
$(() => ajax('/ajax/languageVars', null, 'GET', setupCore, null, null)) $(() => ajax('/ajax/languageVars', null, 'GET', setupCore, null, null))

View File

@@ -10,7 +10,7 @@ interface floorplan{
transformer: Konva.Transformer transformer: Konva.Transformer
tableLayer: Konva.Layer tableLayer: Konva.Layer
rooms: room[] rooms: room[]
tables: table[] tables: floorplan_table[]
decorations: decoration[] decorations: decoration[]
activeTableNumbers: number[] activeTableNumbers: number[]
selectedTableNumber: number selectedTableNumber: number
@@ -24,7 +24,7 @@ interface floorplan{
} }
interface floorplan_data{ interface floorplan_data{
tables: table[] tables: floorplan_table[]
decorations: decoration[] decorations: decoration[]
activeTableNumbers: number[] activeTableNumbers: number[]
rooms: room[] rooms: room[]
@@ -68,6 +68,11 @@ const setupFloorplanEvents = () => {
doc.on('click', '.transferTableButton', toggleTransferMode) doc.on('click', '.transferTableButton', toggleTransferMode)
doc.on('click', '.reserveTableButton', reserveTable) doc.on('click', '.reserveTableButton', reserveTable)
doc.on('click', '.unreserveTableButton', unreserveTable) doc.on('click', '.unreserveTableButton', unreserveTable)
doc.on('click', '.placeOrderButton', placeOrderButtonClicked)
}
const placeOrderButtonClicked = () => {
redirect(`/order/${Floorplan.selectedTableNumber}`)
} }
const roomButtonClicked = (e: Event) => { const roomButtonClicked = (e: Event) => {
@@ -132,9 +137,9 @@ const getRoomById = (roomId: number) => {
) )
} }
const tableIsOpen = (table: table) => Floorplan.activeTableNumbers.includes(table.table_number) const tableIsOpen = (table: floorplan_table) => Floorplan.activeTableNumbers.includes(table.table_number)
const createTableShape = (table: table) => { const createTableShape = (table: floorplan_table) => {
const draggable = isInMode('edit') const draggable = isInMode('edit')
const tableGroup = new Konva.Group({ const tableGroup = new Konva.Group({
@@ -216,6 +221,8 @@ const setupTableEvents = (tableGroup: Konva.Group) => {
tableGroup.on('click', tableClicked) tableGroup.on('click', tableClicked)
tableGroup.on('tap', tableClicked) tableGroup.on('tap', tableClicked)
tableGroup.on('dbltap', tableDblClicked)
tableGroup.on('dblclick', tableDblClicked)
tableGroup.on('dragend', tableGroupTransformed) tableGroup.on('dragend', tableGroupTransformed)
tableShape.on('transformend', tableShapeTransformed) tableShape.on('transformend', tableShapeTransformed)
} }
@@ -236,7 +243,7 @@ const saveTableTransformation = (tableGroup: Konva.Group) => {
const originalTable = getTableDataFromGroup(tableGroup) const originalTable = getTableDataFromGroup(tableGroup)
const tableShape = getTableShapeFromGroup(tableGroup) const tableShape = getTableShapeFromGroup(tableGroup)
const newTableInfo : table = { const newTableInfo : floorplan_table = {
table_number : originalTable.table_number, table_number : originalTable.table_number,
previous_state : originalTable.previous_state, previous_state : originalTable.previous_state,
merged_children : originalTable.merged_children, merged_children : originalTable.merged_children,
@@ -258,7 +265,7 @@ const saveTableTransformation = (tableGroup: Konva.Group) => {
} }
const saveTable = (tableToUpdate: table) => { const saveTable = (tableToUpdate: floorplan_table) => {
const tables = const tables =
Floorplan Floorplan
.tables .tables
@@ -302,20 +309,20 @@ const getTableGroupFromTableNumber = (tableNumber : number) => {
return getTableGroupFromShape(tableShape) return getTableGroupFromShape(tableShape)
} }
const setReservationStatus = (table: table) => { const setReservationStatus = (table: floorplan_table) => {
const reservationText = $('.reservationStatus') const reservationText = $('.reservationStatus')
const tableShape = getTableShapeFromTableNumber(table.table_number) const tableShape = getTableShapeFromTableNumber(table.table_number)
reservationText.text('') reservationText.text('')
if(table.status == 2) { if(table.status == 2) {
tableShape.fill('lightgreen') tableShape.fill('lightgreen')
const reservations = Floorplan.reservations.filter(reservation => reservation.reservation_table_id == table.id) const reservations = Floorplan.reservations.filter(reservation => reservation.floorplan_table_id == table.id)
if (reservations.length) { if (reservations.length) {
turnOnMode('reservedTableSelected') turnOnMode('reservedTableSelected')
reservationText.text(lang('reserved')) reservationText.text(lang('reserved'))
let reservation = reservations[0] let reservation = reservations[0]
if (reservation.reservation_name != '') { if (reservation.name != '') {
reservationText.text(lang('reserved_for', reservation.reservation_name)) reservationText.text(lang('reserved_for', reservation.name))
} }
} }
} else { } else {
@@ -333,11 +340,11 @@ const reserveTable = () => {
const createEmptyReservation = (covers: number) => { const createEmptyReservation = (covers: number) => {
const newReservation: reservation = { const newReservation: reservation = {
id: 0, id: 0,
reservation_covers: covers, covers: covers,
reservation_created_at: 0, created_at: 0,
reservation_table_id: getSelectedTableData().id, floorplan_table_id: getSelectedTableData().id,
reservation_name: '', name: '',
reservation_time: 0, time: 0,
} }
ajax('/ajax/newEmptyReservation', newReservation,'post', emptyReservationCreated, null, null ) ajax('/ajax/newEmptyReservation', newReservation,'post', emptyReservationCreated, null, null )
@@ -347,7 +354,7 @@ const emptyReservationCreated = (reservation: reservation) => {
Floorplan.reservations.push(reservation) Floorplan.reservations.push(reservation)
const selectedTable = getSelectedTableData() const selectedTable = getSelectedTableData()
selectedTable.status = 2 selectedTable.status = 2
selectedTable.default_covers = reservation.reservation_covers selectedTable.default_covers = reservation.covers
updateTableData(selectedTable) updateTableData(selectedTable)
updateCoverText(selectedTable) updateCoverText(selectedTable)
setReservationStatus(getSelectedTableData()) setReservationStatus(getSelectedTableData())
@@ -357,8 +364,8 @@ const emptyReservationCreated = (reservation: reservation) => {
const addReservationName = (name: string) => { const addReservationName = (name: string) => {
hideVirtualKeyboard() hideVirtualKeyboard()
const reservation = Floorplan.reservations.filter(reservation => reservation.reservation_table_id == getSelectedTableData().id)[0] const reservation = Floorplan.reservations.filter(reservation => reservation.floorplan_table_id == getSelectedTableData().id)[0]
reservation.reservation_name = name reservation.name = name
ajax('/ajax/updateReservation', reservation, 'post', reservationNameAdded, null, null) ajax('/ajax/updateReservation', reservation, 'post', reservationNameAdded, null, null)
} }
@@ -369,9 +376,9 @@ const reservationNameAdded = (updatedReservation: reservation) => {
setReservationStatus(getSelectedTableData()) setReservationStatus(getSelectedTableData())
} }
const getReservationsOnTable = (table: table) => Floorplan.reservations.filter(reservation => reservation.reservation_table_id == table.id) const getReservationsOnTable = (table: floorplan_table) => Floorplan.reservations.filter(reservation => reservation.floorplan_table_id == table.id)
const updateTableData = (tableToRemove: table) => { const updateTableData = (tableToRemove: floorplan_table) => {
Floorplan.tables = Floorplan.tables.filter(table => table.id != tableToRemove.id) Floorplan.tables = Floorplan.tables.filter(table => table.id != tableToRemove.id)
Floorplan.tables.push(tableToRemove) Floorplan.tables.push(tableToRemove)
} }
@@ -382,8 +389,8 @@ const unreserveTable = () => {
ajax('/ajax/unreserveTable', selectedTable, 'post', tableUnreserved, null, null) ajax('/ajax/unreserveTable', selectedTable, 'post', tableUnreserved, null, null)
} }
const tableUnreserved = (table: table) => { const tableUnreserved = (table: floorplan_table) => {
Floorplan.reservations = Floorplan.reservations.filter(reservation => reservation.reservation_table_id != table.id) Floorplan.reservations = Floorplan.reservations.filter(reservation => reservation.floorplan_table_id != table.id)
updateTableData(table) updateTableData(table)
setReservationStatus(table) setReservationStatus(table)
} }
@@ -431,7 +438,14 @@ const selectTable = (tableShape: Konva.Shape) => {
turnOnMode('tableSelected') turnOnMode('tableSelected')
} }
const updateCoverText = (table:table) => $('.selectedTableCovers').text(lang('covers', table.default_covers.toString())) const updateCoverText = (table:floorplan_table) => $('.selectedTableCovers').text(lang('covers', table.default_covers.toString()))
const tableDblClicked = (event: Konva.KonvaEventObject<any>) => {
let tableShape = getTableShapeFromGroup(event.currentTarget as Konva.Group)
const table = getTableDataFromShape(tableShape)
redirect(`/order/${table.table_number}`)
}
const tableClicked = (event: Konva.KonvaEventObject<any>) => { const tableClicked = (event: Konva.KonvaEventObject<any>) => {
let tableShape = getTableShapeFromGroup(event.currentTarget as Konva.Group) let tableShape = getTableShapeFromGroup(event.currentTarget as Konva.Group)
@@ -704,7 +718,7 @@ const redrawTable = (tableGroup: Konva.Group) => {
const showAddTablePopup = () => showVirtualNumpad(lang('new_table_number'), 4, false, false, true, addTable); const showAddTablePopup = () => showVirtualNumpad(lang('new_table_number'), 4, false, false, true, addTable);
const addTable = (tableNumber: number) => { const addTable = (tableNumber: number) => {
const newTable : table = { const newTable : floorplan_table = {
id: 0, id: 0,
table_number: tableNumber, table_number: tableNumber,
room_id: Floorplan.currentRoom.id, room_id: Floorplan.currentRoom.id,
@@ -724,7 +738,7 @@ const addTable = (tableNumber: number) => {
ajax('/ajax/createTable', newTable, 'post', tableAdded, tableNotAdded, null) ajax('/ajax/createTable', newTable, 'post', tableAdded, tableNotAdded, null)
} }
const tableAdded = (table: table) => { const tableAdded = (table: floorplan_table) => {
deselectTables() deselectTables()
const newTableGroup = createTableShape(table) const newTableGroup = createTableShape(table)
Floorplan.tables.push(table) Floorplan.tables.push(table)
@@ -752,7 +766,7 @@ const deleteTable = (tableNumber: number) => {
ajax(`/ajax/deleteTable`, tableToDelete, 'post', tableDeleted, null, null); ajax(`/ajax/deleteTable`, tableToDelete, 'post', tableDeleted, null, null);
} }
const tableDeleted = (deletedTable: table) => { const tableDeleted = (deletedTable: floorplan_table) => {
Floorplan.tables = Floorplan.tables.filter(table => table.table_number != deletedTable.table_number) Floorplan.tables = Floorplan.tables.filter(table => table.table_number != deletedTable.table_number)
const tableGroup = getTableGroupFromTableNumber(deletedTable.table_number) const tableGroup = getTableGroupFromTableNumber(deletedTable.table_number)
deselectTables() deselectTables()
@@ -762,7 +776,7 @@ const tableDeleted = (deletedTable: table) => {
const toggleMergeMode = () => toggleMode('merge') const toggleMergeMode = () => toggleMode('merge')
const mergeTables = (table1: table, table2: table ) => { const mergeTables = (table1: floorplan_table, table2: floorplan_table ) => {
toggleMergeMode() toggleMergeMode()
if(table1.table_number == table2.table_number){ if(table1.table_number == table2.table_number){
posAlert(lang('error_self_merge')) posAlert(lang('error_self_merge'))
@@ -771,7 +785,7 @@ const mergeTables = (table1: table, table2: table ) => {
ajax('/ajax/mergeTables', [table1, table2], 'post', tablesMerged, null, null) ajax('/ajax/mergeTables', [table1, table2], 'post', tablesMerged, null, null)
} }
const tablesMerged = (tables: Record<'child'|'parent'|'merged', table>) => { const tablesMerged = (tables: Record<'child'|'parent'|'merged', floorplan_table>) => {
tableDeleted(tables['child']) tableDeleted(tables['child'])
tableDeleted(tables['parent']) tableDeleted(tables['parent'])
tableAdded(tables['merged']) tableAdded(tables['merged'])
@@ -783,7 +797,7 @@ const tablesMerged = (tables: Record<'child'|'parent'|'merged', table>) => {
const unmergeTable = () => ajax(`/ajax/unmergeTable/${Floorplan.selectedTableNumber}`, null, 'get', tablesUnmerged, null, null) const unmergeTable = () => ajax(`/ajax/unmergeTable/${Floorplan.selectedTableNumber}`, null, 'get', tablesUnmerged, null, null)
const tablesUnmerged = (tables: Record<'child'|'parent', table>) => { const tablesUnmerged = (tables: Record<'child'|'parent', floorplan_table>) => {
const parentTable = tables['parent'] const parentTable = tables['parent']
const childTable = tables['child'] const childTable = tables['child']
@@ -795,7 +809,7 @@ const tablesUnmerged = (tables: Record<'child'|'parent', table>) => {
const toggleTransferMode = () => toggleMode('transfer') const toggleTransferMode = () => toggleMode('transfer')
const transferTables = (origin: table, destination: table) => { const transferTables = (origin: floorplan_table, destination: floorplan_table) => {
if(origin.table_number == destination.table_number){ if(origin.table_number == destination.table_number){
posAlert(lang('transfer_self_error')) posAlert(lang('transfer_self_error'))
return return
@@ -804,7 +818,7 @@ const transferTables = (origin: table, destination: table) => {
ajax(`/ajax/transferTable/${origin.table_number}/${destination.table_number}`, null, 'get', tableTransferred, null, null) ajax(`/ajax/transferTable/${origin.table_number}/${destination.table_number}`, null, 'get', tableTransferred, null, null)
} }
const tableTransferred = (tables: Record<"origin"|"destination", table>) => { const tableTransferred = (tables: Record<"origin"|"destination", floorplan_table>) => {
const origin = tables['origin'] const origin = tables['origin']
const destination = tables['destination'] const destination = tables['destination']

View File

@@ -1,19 +1,54 @@
interface OrderScreen{ type OrderScreenData = {
order_screen_pages: order_screen_page[] order_screen_pages: order_screen_page[]
sales_categories: sales_category[]
print_groups: print_group[]
custom_item: item
}
type OrderScreen = {
order_screen_pages: order_screen_page[]
last_added_item: orderItem
order_items: orderItem[]
sales_categories: sales_category[]
print_groups: print_group[]
order_item_id_generator: Generator
selected_item_ids: number[]
qty_override: number
print_group_override: print_group
custom_item: item,
selected_cover: number
table: floorplan_table,
} }
let OrderScreen : OrderScreen = { let OrderScreen : OrderScreen = {
order_screen_pages: null order_screen_pages: null,
last_added_item: null,
order_items: [],
print_groups: [],
sales_categories: [],
order_item_id_generator: newestId(),
selected_item_ids: [],
qty_override: 1,
print_group_override: null,
custom_item: null,
selected_cover: 0,
table: null,
} }
const loadPageGroup = (e: Event) => { const loadPageGroup = (e: Event) => {
let button = $(e.target) const button = $(e.target)
const container = $('#pageGroupContainer')
$('.loadPageGroup').removeClass('active') $('.loadPageGroup').removeClass('active')
button.addClass('active') button.addClass('active')
let pageGroupId = button.data('page-group-id') let pageGroupId = button.data('page-group-id')
$('.pageGroup').hide()
container.find('.pageGroup').hide()
let activeGrid = $(`.pageGroup[data-page-group-id=${pageGroupId}]`) let activeGrid = $(`.pageGroup[data-page-group-id=${pageGroupId}]`)
let navButtons = $('.pageNavigation') let navButtons = container.next('.pageNavigation')
navButtons.css('display', 'flex')
activeGrid.find('.gridPage').length > 1 activeGrid.find('.gridPage').length > 1
? navButtons.show() ? navButtons.show()
@@ -22,26 +57,548 @@ const loadPageGroup = (e: Event) => {
activeGrid.css('display', 'inline-flex') activeGrid.css('display', 'inline-flex')
} }
const setupOrderScreen = (data: OrderScreen) => { const setupOrderScreen = (data: OrderScreenData) => {
OrderScreen = data
$('.coverSelector, .gridContainer').hide()
OrderScreen.order_screen_pages = data.order_screen_pages
OrderScreen.sales_categories = data.sales_categories
OrderScreen.print_groups = data.print_groups
OrderScreen.custom_item = data.custom_item
updateOrderBoxTotals()
let doc = $(document) let doc = $(document)
doc.on('click', '.nextButton', goToNextPage) doc.on('click', '.nextButton', goToNextPage)
doc.on('click', '.prevButton', goToPrevPage) doc.on('click', '.prevButton', goToPrevPage)
doc.on('click', '.loadPageGroup', loadPageGroup) doc.on('click', '.loadPageGroup', loadPageGroup)
doc.on('click', '[data-primary-action=item]', itemButtonClicked)
doc.on('click', '[data-primary-action=grid],[data-secondary-action=grid]', gridButtonClicked)
doc.on('click', '.closeGrid', hideGrids)
doc.on('click', '.freetextButton', freetext)
doc.on('click', '.openItemButton', customItem)
doc.on('click', '.orderBoxTable tbody tr', itemRowClicked)
doc.on('click', '.voidButton', voidButtonClicked)
doc.on('dblclick', '.voidButton', voidLastItem)
doc.on('click', '.numpadButton', overrideQty)
doc.on('click', '.accumulateButton', () => toggleMode('accumulate'))
doc.on('click', '.changeCoverNumberButton', changeCoverNumberPrompt)
doc.on('click', '.showCoverSelectorButton', showCoverSelector)
doc.on('click', '.coverSelectorButton', coverSelected)
doc.on('change', '[name=print_override]', printGroupOverride)
turnOnMode('accumulate')
$('.loadPageGroup').first().trigger('click') $('.loadPageGroup').first().trigger('click')
let observer = new window.MutationObserver((mutations, observer) => updateOrderBoxTotals())
observer.observe($('.orderBoxTable tbody').get()[0], {
subtree: true,
attributes: true,
childList: true
});
} }
/** /**
* @param direction 1 for forward, -1 for backwards. * @param direction 1 for forward, -1 for backwards.
* @param button
*/ */
const navigatePage = (direction: number) => { const navigatePage = (direction: number, button: JQuery) => {
let grid = $('.pageGroup:visible') const grid =
button
.parent()
.parent()
.find('.pageGroup:visible')
grid.get()[0].scrollLeft += grid.width() * direction grid.get()[0].scrollLeft += grid.width() * direction
} }
const goToNextPage = () => navigatePage(1) const goToNextPage = (e: JQuery.TriggeredEvent) => navigatePage(1, $(e.target))
const goToPrevPage = () => navigatePage(-1) const goToPrevPage = (e: JQuery.TriggeredEvent) => navigatePage(-1, $(e.target))
const addItemToOrderBox = (orderItem:orderItem) => {
const orderBox = $('.orderBoxTable tbody')
let selectedRows = orderBox.find('tr.selected')
let lastRow : JQuery = selectedRows.length ? getLastInstructionRow(selectedRows.first()) : orderBox.find('tr').last()
const existingRow = orderBox
.find('tr')
.filterByData('item', orderItem.item)
.filterByData('print_group', orderItem.print_group)
.filterByData('cover', orderItem.cover)
.last()
$(() => ajax('/orderScreen/getOrderScreenData/1', null, 'get', setupOrderScreen, null, null) ) //If accumulating, just increase the quantity of the existing row.
if(existingRow.length > 0 && isInMode('accumulate')){
incrementRowQty(existingRow, orderItem.qty)
scrollToElement(existingRow)
existingRow.pulse()
} else {
const newRow = createOrderRow(orderItem)
lastRow.length > 0
? lastRow.after(newRow)
: orderBox.append(newRow)
scrollToElement(newRow)
newRow.pulse()
}
deselectRow(orderBox.find('tr'))
}
const addInstructionToOrderBox = (instruction: orderItem) => {
const orderBox = $('.orderBoxTable tbody')
let selectedRows = orderBox.find('tr.selected')
const newRow = createOrderRow(instruction)
//If no items are added, then you can't add an instruction row.
if(!orderBox.find('tr.itemRow').length) return
if(selectedRows.length > 0){
selectedRows.each( (_, row) => {
const selectedRow = $(row)
const parentRow = getParentRow(selectedRow)
if(parentRow.is(selectedRow) || !parentRow.hasClass('selected')) {
const newRow = createOrderRow(instruction)
getLastInstructionRow(selectedRow).after(newRow)
newRow
.setColumnValue( lang('printgroup_header'), selectedRow.getColumnValue(lang('printgroup_header')) )
if(parentRow.hasClass('selected')){
selectRow(newRow)
} else {
newRow.pulse()
}
scrollToElement(newRow)
}
})
}
const lastRow = orderBox.find('tr').last()
newRow
.setColumnValue(lang('printgroup_header'), lastRow.getColumnValue(lang('printgroup_header')))
.appendTo(orderBox)
.pulse()
scrollToElement(newRow)
}
const addNewItem = (item: item, qty = 1) => {
const salesCategory = OrderScreen.sales_categories.where('id', item.item_category)
const printGroup = OrderScreen.print_group_override ?? OrderScreen.print_groups.where('id', salesCategory.print_group)
const orderItem : orderItem = {
id: OrderScreen.order_item_id_generator.next().value,
item: item,
qty: qty,
print_group: printGroup,
cover: OrderScreen.selected_cover,
}
switch(item.item_type){
case 'instruction':
addInstructionToOrderBox(orderItem)
break
case 'item':
default:
addItemToOrderBox(orderItem)
break
}
}
const getLastInstructionRow = (row: JQuery) => {
let stopCounting = false
let finalRow = row
row.nextAll().each(function (index, activeRow){
if(!stopCounting){
if($(activeRow).hasClass('instructionRow')){
finalRow = $(activeRow)
} else {
stopCounting = true
}
}
})
return $(finalRow)
}
const getParentRow = (row: JQuery) => {
return row.hasClass('instructionRow')
? row.prevAll('.itemRow').first()
: row
}
const incrementRowQty = (row: JQuery, qty: number) => {
const existingQty = Number(row.getColumnValue(lang('qty_header')))
const newQty = qty + existingQty
row.setColumnValue(lang('qty_header'), newQty)
calculateRowTotal(row)
}
const renderOrderBox = () => {
const orderBox = $('.orderBoxTable')
const tbody = orderBox.children('tbody')
const newTbody = $('<tbody />')
OrderScreen.order_items.forEach(orderItem => {
const newRow = createOrderRow(orderItem)
newTbody.append(newRow)
newRow.pulse()
if(OrderScreen.selected_item_ids.includes(orderItem.id)){
selectRow(newRow)
}
})
tbody.replaceWith(newTbody)
const element = orderBox.find('tbody tr').last().get()[0]
element.scrollIntoView()
OrderScreen.last_added_item = null
}
const createOrderRow = (orderItem: orderItem) => {
const row = $('.orderBoxTable').EmptyRow()
const price = money(orderItem.item.price1)
const itemCellText = $('<span/>').text(orderItem.item.item_name)
row
.addClass(`${orderItem.item.item_type}Row`)
.setColumnValue(lang('qty_header'), orderItem.qty)
.setColumnValue(lang('price_header'), price)
.setColumnValue(lang('id_header'), orderItem.item.id)
.setColumnValue(lang('total_price_header'), price.multiply(orderItem.qty))
.setColumnValue(lang('printgroup_header'), orderItem.print_group?.name)
.data('order-item-id', orderItem.id)
.data('order-item-id', orderItem.id)
.data('print_group', orderItem.print_group)
.data('cover', orderItem.cover)
.data('item', orderItem.item)
.find('td.itemCell')
.append(itemCellText)
changeCoverOnRow(row, orderItem.cover)
if(orderItem.item.item_type == 'instruction' && price.value <= 0){
row
.find('.totalPriceCell')
.css('font-size', 0)
}
return row
}
const itemButtonClicked = (e: JQuery.TriggeredEvent) => {
hideGrids()
const existingItemRows = $('.itemRow')
const button = $(e.target).closest('.posButton')
const item : item = button.data('item')
if(item.item_type == 'instruction' && existingItemRows.length < 1) return
const qty = OrderScreen.qty_override || 1
OrderScreen.qty_override = 1
addNewItem(item, qty)
}
const gridButtonClicked = (e: JQuery.TriggeredEvent) => {
const button = $(e.target).closest('.posButton')
const grid : number = button.data('grid')
ajax(`/orderScreen/getGridHtml/${grid}`, null, null,gridHtmlGenerated, null, null)
}
const hideGrids = () => $('.gridContainer').hide()
const gridHtmlGenerated = (gridData: {gridHtml:string, grid: grid}) => {
const gridContainer = $('.gridContainer')
const gridCellWidth = getGridCellWidth()
const gridCellHeight = getGridCellHeight()
const grid = gridData.grid
const gridHtml = gridData.gridHtml
gridContainer
.show()
.width(gridCellWidth * grid.grid_cols)
.children('.gridContainerHeader')
.children('span')
.text(grid.grid_name)
.parent()
.parent()
.find('.pageGroup')
.html(gridHtml)
.show()
.parent()
.height(gridCellHeight * grid.grid_rows)
.closest('.gridContainer')
.find('.pageNavigation')
.toggle(gridContainer.find('.gridPage').length > 1)
.height(gridCellHeight)
}
const itemRowClicked = (e: JQuery.TriggeredEvent) => {
const row = $(e.target).closest('tr')
if(isInMode('void')){
voidRows(row)
turnOffMode('void')
return
}
if(!row.hasClass('selected')) selectRow(row)
else deselectRow(row)
}
const selectRow = (row: JQuery) => {
row.addClass('selected')
const instructionRows = row.nextUntil('.itemRow')
if(row.hasClass('itemRow') && instructionRows.length){
instructionRows.each((index, row) => {
selectRow($(row))
})
}
}
const deselectRow = (row: JQuery) => {
row.removeClass('selected')
const instructionRows = row.nextUntil('.itemRow')
if(row.hasClass('itemRow') && instructionRows.length){
deselectRow(instructionRows)
}
}
const deleteRow = (row: JQuery) => row.find('*:not(.hidden)').slideUp('fast', () => row.remove())
const voidInstructionRow = (row: JQuery) => {
if(!row.prevAll('.itemRow').first().hasClass('selected'))
deleteRow(row)
}
const voidItemRow = (row : JQuery) => decrementQty(row)
const voidRow = (row: JQuery) => {
if(row.hasClass('itemRow')) voidItemRow(row)
else voidInstructionRow(row)
}
const voidRows = (rows: JQuery) => rows.each((index, row) => voidRow($(row)))
const voidButtonClicked = () => {
const selectedRows = $('.orderBox tr.selected')
if(isInMode('void')){
turnOffMode('void')
} else if(selectedRows.length){
voidRows(selectedRows)
} else {
turnOnMode('void')
}
}
const voidLastItem = () => {
const orderBox = $('.orderBoxTable tbody')
const allRows = orderBox.find('tr')
if(allRows.length < 1) return
voidRows(allRows.last())
}
const updateOrderBoxTotals = () => {
const allRows = $('.orderBoxTable tbody tr')
const selectedRows = $('.orderBoxTable tbody tr.selected')
const completeTotal = lang('totalPrice', getTotalOfRows(allRows))
const selectedTotal = lang('selectedPrice', getTotalOfRows(selectedRows))
$('.orderBoxTotal').text(completeTotal)
$('.orderBoxSelectedTotal').text(selectedTotal)
}
const getTotalOfRows = (rows: JQuery) => {
return money(rows
.find('td.totalPriceCell')
.get()
.map(cell => Number(cell.innerText))
.filter(number => !isNaN(number))
.reduce( (total, number) => total + number , 0), false)
.format()
}
const getQty = (row: JQuery) => Number(row.getColumnValue(lang('qty_header')))
const getUnitPrice = (row: JQuery) => moneyFromString(row.getColumnValue(lang('price_header')))
const calculateRowTotal = (row: JQuery) => {
let price = getUnitPrice(row)
let qty = getQty(row)
row.setColumnValue(lang('total_price_header'), price.multiply(qty))
}
const decrementQty = (row: JQuery, qty=1) => {
const existingQty = getQty(row)
if(existingQty <= 1){
const childRows = row.nextUntil('.itemRow')
deleteRow(row)
deleteRow(childRows)
return
}
row.setColumnValue(lang('qty_header'), existingQty - qty)
calculateRowTotal(row)
}
const scrollToElement = (JQueryElement: JQuery) => {
const element = JQueryElement.get()[0]
const container = JQueryElement.closest('.orderBox').get()[0]
const containerTop = $(container).scrollTop()
const containerBottom = containerTop + $(container).height();
const elemTop = element.offsetTop
const elemBottom = elemTop + $(element).height();
if (elemTop < containerTop) {
$(container).scrollTop(elemTop);
} else if (elemBottom > containerBottom) {
$(container).scrollTop(elemBottom - $(container).height());
}
}
const overrideQty = () => showVirtualNumpad(lang('multiplier'), 4, false, true, true, qtyOverridden)
const qtyOverridden = (qtyString: string) => OrderScreen.qty_override = Number(qtyString)
const printGroupOverride = (e: JQuery.TriggeredEvent) => {
const input = $(e.target)
const printGroupId = Number(input.val())
const orderBox = $('.orderBoxTable tbody')
const selectedRows = orderBox.find('tr.selected')
const newPrintGroup = OrderScreen.print_groups.where('id', printGroupId)
if(selectedRows.length && newPrintGroup){
selectedRows.each((index, row) => {
$(row).setColumnValue(lang('printgroup_header'), newPrintGroup.name)
$(row).data('print_group', newPrintGroup)
})
OrderScreen.print_group_override = null
resetToggle(input)
} else {
OrderScreen.print_group_override = newPrintGroup
}
}
const freetext = () => showVirtualKeyboard('', 32,false, freetextSubmitted)
const freetextSubmitted = (text: string) => {
if(text.trim().length < 1) return
if($('.orderBoxTable tbody tr').length < 1){
posAlert(lang('freetext_no_order'))
}
const item = Object.assign({}, OrderScreen.custom_item)
item.item_type = 'instruction'
item.item_name = text
addNewItem(item)
}
const customItem = () => showVirtualKeyboard(lang('enter_item_name'), 32,false, customItemTextSubmitted)
const customItemTextSubmitted = (text: string) => {
const submitFunction = (priceString: string) => {
const price = currency(priceString, {fromCents: false})
const item = Object.assign({}, OrderScreen.custom_item)
item.item_type = 'item'
item.item_name = text
item.price1 = price.intValue
addNewItem(item)
}
showVirtualNumpad(lang('enter_item_price'), 4, false, true, true, submitFunction)
}
const getGridCellHeight = () => $('#pageGroupContainer').height()/8
const getGridCellWidth = () => $('#pageGroupContainer').width()/6
const showCoverSelector = (event: JQuery.TriggeredEvent) => {
const button = $(event.target)
const gridHeight = getGridCellHeight()
const coverSelector = $('.coverSelector')
coverSelector
.toggle(!coverSelector.is(':visible'))
.width(button.width())
.css({
left: button.offset().left + 'px',
top: button.offset().top + button.height() + 'px',
})
.find('.coverSelectorButton')
.height(gridHeight)
}
const coverSelected = (event: JQuery.TriggeredEvent) => {
$('.coverSelector').hide()
const button = $(event.target)
const cover = Number(button.data('cover'))
const selectedRows = $('.orderBoxTable tbody').find('tr.itemRow.selected')
selectedRows.each( (_, selectedRow) => changeCoverOnRow($(selectedRow), cover))
OrderScreen.selected_cover = cover
}
const changeCoverOnRow = (row: JQuery, cover: number) => {
row.data('cover', cover)
const itemCell = row.find('.itemCell')
const existingCoverSpan = itemCell.find('small')
const coverSpan = existingCoverSpan.length > 0
? existingCoverSpan
: $('<small/>').appendTo(itemCell)
coverSpan.text(lang('selected_cover', cover.toString()))
if(cover < 1 || !row.hasClass('itemRow')) {
coverSpan.remove()
}
}
const changeCoverNumberPrompt = () =>
showVirtualNumpad(lang('how_many_covers'), 3, false, false, true, changeCoverNumberPromptSubmitted)
const changeCoverNumberPromptSubmitted = (value: string) => updateCoverNumbers(Number(value))
const updateCoverNumbers = (covers: number) => {
let newTable = Object.assign({}, OrderScreen.table)
newTable.default_covers = covers
ajax('/orderScreen/updateCovers', newTable, 'post', coverNumbersUpdated, null, null)
}
const coverNumbersUpdated = (newTable: floorplan_table) => {
const covers = newTable.default_covers
OrderScreen.table = newTable
$('.changeCoverNumberButton').text(lang('covers', covers.toString()))
generateCoverSelector()
}
const generateCoverSelector = () => {
const covers = OrderScreen.table.default_covers
const coverSelector = $('.coverSelector')
coverSelector.hide().children().remove()
for(let cover=0; cover<=covers; cover++) {
const buttonText = cover==0 ? lang('cover_zero') : lang('selected_cover', cover.toString())
loadTemplate('#posButtonTemplate')
.find('a')
.first()
.addClass('coverSelectorButton')
.text(buttonText)
.data('cover', cover)
.appendTo(coverSelector)
}
}
$(() => {
OrderScreen.table = $('#pageContainer').data('table') || null
ajax('/orderScreen/getOrderScreenData/1', null, 'get', setupOrderScreen, null, null)
})

View File

@@ -0,0 +1,53 @@
interface JQuery {
getColumnValue(columnHeading: string) : string
setColumnValue(columnHeading: string, value: any) : JQuery
getColumnIndex(columnHeading: string) : number
EmptyRow() : JQuery<HTMLTableRowElement>
filterByData(prop: string, value: any) : JQuery
pulse() : JQuery
}
$.fn.pulse = function(this: JQuery) {
pulseElement(this)
return this
}
$.fn.EmptyRow = function(this: JQuery) {
const headingRow = this.find('th').first().closest('tr')
const headingCells = headingRow.find('th')
const newRow = $('<tr/>')
headingCells.each( (index, cell) => {
const newCell = $('<td/>')
const attributes = Array.from(cell.attributes)
newCell.data('column', cell.innerText.trim())
newCell.data('column-index', index)
attributes.forEach(attribute => newCell.attr(attribute.name, attribute.value))
newRow.append(newCell)
})
return newRow as JQuery<HTMLTableRowElement>
}
$.fn.getColumnIndex = function(this: JQuery, columnHeading: string){
return this
.find('td')
.filterByData('column', columnHeading)
.data('column-index')
}
$.fn.getColumnValue = function(this: JQuery, columnHeading: string){
return this.find('td').filterByData('column', columnHeading).text()
}
$.fn.setColumnValue = function(this: JQuery, columnHeading: string, value: any){
this.find('td').filterByData('column', columnHeading).text(value)
return this
}
$.fn.filterByData = function(prop: string, val: any) {
return this.filter(
function() {
return $(this).data(prop)==val
}
)
}

View File

@@ -1,6 +1,26 @@
type PosMode = "edit" | "void" | "transfer" | "default" | "tableSelected" | "decorationSelected" | "activeTableSelected" | "merge" | "reservedTableSelected" type PosMode = "edit" | "void" | "transfer" | "default" | "tableSelected" | "decorationSelected" | "activeTableSelected" | "merge" | "reservedTableSelected" | "accumulate"
type PosModes = PosMode[] type PosModes = PosMode[]
interface order {
clerk: string
split: boolean
items: orderItem[]
}
interface orderItem {
id: number
qty: number
print_group: print_group
item: item
cover: number
}
interface print_group {
id: number,
name: string,
printer: number,
venue_id: number,
}
interface ajaxResult { interface ajaxResult {
status: string status: string
@@ -13,7 +33,7 @@ interface ApplicationState {
languageVars: Record<any, string> languageVars: Record<any, string>
} }
interface table { interface floorplan_table {
table_number: number, table_number: number,
room_id: number room_id: number
venue_id: number venue_id: number
@@ -52,11 +72,11 @@ interface room {
interface reservation { interface reservation {
id: number, id: number,
reservation_name: string, name: string,
reservation_time: number, time: number,
reservation_covers: number, covers: number,
reservation_created_at: number, created_at: number,
reservation_table_id: number, floorplan_table_id: number,
} }
interface keyboard { interface keyboard {
@@ -69,3 +89,27 @@ interface keyboard {
interface order_screen_page{id: number; order_screen_page_group_id: number; grid_id: number} interface order_screen_page{id: number; order_screen_page_group_id: number; grid_id: number}
interface grid {id: number; grid_name: string; grid_rows: number; grid_cols: number; grid_data: string} interface grid {id: number; grid_name: string; grid_rows: number; grid_cols: number; grid_data: string}
interface item {
id: number
item_code: string
item_category: number
item_name: string
item_type: string
price1: number
price2: number
price3: number
price4: number
price5: number
}
type sales_category = {
id: number
parent: number
name: string
print_group: string
venue_id: number
}
interface Array<T> {
where(property: string, value: any): T
}

View File

@@ -15,3 +15,12 @@
--posbutton-background-active: #20282D -webkit-gradient(linear, left top, left bottom, color-stop(3%,#20282D), color-stop(51%,#252E34), color-stop(100%,#222A30)) 0 top --posbutton-background-active: #20282D -webkit-gradient(linear, left top, left bottom, color-stop(3%,#20282D), color-stop(51%,#252E34), color-stop(100%,#222A30)) 0 top
--void-button-background: red -webkit-gradient(linear, left top, left bottom, color-stop(3%,darkred), color-stop(51%,darkred), color-stop(100%,red)) 0 top --void-button-background: red -webkit-gradient(linear, left top, left bottom, color-stop(3%,darkred), color-stop(51%,darkred), color-stop(100%,red)) 0 top
/** Order Screen **/
--orderbox-header-background: #888
--orderbox-row-background: var(--pos-header-background)
--orderbox-selected-row-background: #dd7f37
/** Order Box/Payment Splitter Box **/
--pulse-first-color: #ffa93e
--pulse-second-color: #dd7f37
--pulse-final-color: var(--orderbox-row-background)

View File

@@ -3,6 +3,15 @@
src: url("/fonts/OpenSans-Regular.ttf") format('truetype') src: url("/fonts/OpenSans-Regular.ttf") format('truetype')
font-style: normal font-style: normal
@font-face
font-family: "manrope"
src: url("/fonts/OpenSans-SemiBold.ttf") format('truetype')
font-weight: bold
@font-face
font-family: "manrope"
src: url("/fonts/OpenSans-Light.ttf") format('truetype')
font-weight: 100
* *
margin: 0 margin: 0
@@ -11,9 +20,19 @@
font-family: 'manrope', sans-serif font-family: 'manrope', sans-serif
scroll-behavior: smooth scroll-behavior: smooth
*:not(input, textarea)
-webkit-touch-callout: none
-webkit-user-select: none
-khtml-user-select: none
-moz-user-select: none
-ms-user-select: none
user-select: none
.rtl .rtl
direction: rtl direction: rtl
input[type=text], select, textarea input[type=text], select, textarea
padding-left: 1em padding-left: 1em
padding-right: 1em padding-right: 1em
@@ -28,6 +47,7 @@ input[type=text], select, textarea
bottom: 0 bottom: 0
left: 0 left: 0
right: 0 right: 0
z-index: 999
@mixin mobile @mixin mobile
@media screen and (max-width: 900px) @media screen and (max-width: 900px)
@@ -80,9 +100,26 @@ body
.posButton.active, .posButton:active .posButton.active, .posButton:active
border: inset 2px border: inset 2px
background: var(--posbutton-background-active)
.posButton.voidButton .posButton.voidButton
background: var(--void-button-background) background: var(--void-button-background)
.invisible .invisible
visibility: hidden visibility: hidden
.hidden
display: none
.pulse
animation-name: color
animation-duration: 300ms
animation-iteration-count: 1
@keyframes color
0%
background-color: var(--pulse-first-color)
50%
background-color: var(--pulse-second-color)
100%
background-color: var(--pulse-final-color)

View File

@@ -26,13 +26,35 @@
.orderBox .orderBox
flex-basis: 75% flex-basis: 75%
background: var(--global-bgcolor) background: var(--global-bgcolor)
overflow-y: auto
.orderBoxInfo .orderBoxInfo
@include flex
flex-basis: 5% flex-basis: 5%
background-color: white background-color: white
.voidModeWarning
@include flex
@include flex-item
color: red
font-weight: bold
.orderBoxFooter .orderBoxFooter
flex-basis: 10% flex-basis: 10%
@include flex-column
> *
@include flex
@include flex-column-item
justify-content: flex-end
padding: 0 0.7em
> .orderBoxTotal
align-items: flex-end
font-size: 1.3em
> .orderBoxSelectedTotal
align-items: flex-start
font-size: 0.9em
#rightColumn #rightColumn
@include flex-column @include flex-column
@@ -48,32 +70,28 @@
flex-grow: 0 flex-grow: 0
flex-shrink: 0 flex-shrink: 0
.utilityButtons
@include flex-column-item
@include flex
flex-basis: 20%
> *
@include flex-item
@include flex
flex-basis: 30%
.logoutButton
flex-basis: 10%
.functionButtons .functionButtons
@include flex-column-item @include flex-column-item
@include flex @include flex
flex-basis: 80% flex-basis: 100%
> * > .functionColumn
@include flex-item @include flex-item
@include flex-column @include flex-column
flex-basis: 25%
> * > *
@include flex-column-item @include flex-column-item
> .printGroupButtons
flex-basis: 25%
height: 100%
display: grid
grid-template-columns: repeat(2, 1fr)
grid-auto-rows: auto
> *
padding: 0.5em
#pageList #pageList
@include flex @include flex
@@ -90,7 +108,7 @@
.active .active
border-bottom: none border-bottom: none
#pageContainer #pageGroupContainer
@include flex-column @include flex-column
@include flex-column-item @include flex-column-item
justify-content: flex-end justify-content: flex-end
@@ -116,8 +134,6 @@
flex-shrink: 0 flex-shrink: 0
flex-grow: 0 flex-grow: 0
display: grid display: grid
grid-template-columns: repeat(6, 1fr)
grid-template-rows: repeat(8, 1fr)
.doubleWidth .doubleWidth
width: calc(200%) width: calc(200%)
@@ -153,13 +169,20 @@
.buttonImg .buttonImg
padding: 0.6em padding: 0.6em
flex-basis: 80% flex-basis: 65%
width: 100% width: 100%
flex-shrink: 0
flex-grow: 0
.text .text
@include flex @include flex
flex-basis: 20% align-items: flex-start
flex-grow: 0
flex-shrink: 0
flex-basis: 35%
width: 100% width: 100%
overflow: hidden
font-size: 0.9em
.hasImage.doubleWidth .hasImage.doubleWidth
flex-direction: row flex-direction: row
@@ -189,11 +212,132 @@
flex-basis: 50% flex-basis: 50%
height: 100% height: 100%
.pageNavigation .pageNavigation
@include flex @include flex
@include flex-column-item @include flex-column-item
display: none
flex-basis: 15%
> * > *
@include flex-item @include flex-item
.coverSelector
display: flex
flex-wrap: wrap
border: 2px solid var(--global-border-color)
background: var(--global-bgcolor)
position: absolute
z-index: 100
.coverSelectorButton
flex-basis: 50%
.coverSelectorButton:first-of-type
flex-basis: 100%
.orderBoxTable
width: 100%
border-collapse: collapse
tr
background: var(--orderbox-row-background)
.selected
background: var(--orderbox-selected-row-background)
thead tr
background: var(--orderbox-header-background)
th
font-weight: normal
text-align: center
padding: 0.2em 0.5em
tr
td, th
text-align: center
font-size: 0.9em
td
padding: 1em 0.5em
font-weight: bold
.itemCell
text-align: center
width: 60%
.qtyCell
width: 10%
.printGroupCell
width: 20%
.totalPriceCell
width: 10%
td.itemCell
text-align: left
display: flex
flex-direction: column
tr.instructionRow
td
font-weight: 100
td.itemCell
padding-left: 2em
small
font-size: 0
.qtyCell, .printGroupCell
font-size: 0
.gridContainer
background: var(--global-bgcolor)
border: var(--global-border-color) solid 2px
z-index: 500
margin: auto
position: absolute
left: 0
right: 0
top: 50%
transform: translateY(-50%)
.gridContainerHeader
height: 3em
width: 100%
@include flex
background-color: var(--global-secondary-bgcolor)
border-bottom: solid 1px var(--global-border-color)
> *
@include flex
height: 100%
span
flex-basis: 90%
.closeGrid
flex-basis: 10%
.gridContainerGrid
width: 100%
@include flex-column
scrollbar-width: none
-ms-overflow-style: none
::-webkit-scrollbar
display: none
.pageGroup
display: inline-flex
flex-basis: 100%
flex-grow: 0
overflow-x: auto
.gridPage
width: 100%
height: 100%
flex-shrink: 0
flex-grow: 0
display: grid

View File

@@ -0,0 +1,3 @@
<a class="posButton <!--[var:classes]-->" <!--[var: attributes]-->>
<!--[var:text]-->
</a>

View File

@@ -34,7 +34,7 @@
<a class="reservationStatus" data-visible-in-mode='["reservedTableSelected"]'></a> <a class="reservationStatus" data-visible-in-mode='["reservedTableSelected"]'></a>
<small class="selectedTableCovers"></small> <small class="selectedTableCovers"></small>
</div> </div>
<a class="posButton"><!--[lang:order_table]--></a> <a class="posButton placeOrderButton"><!--[lang:order_table]--></a>
<a class="posButton reserveTableButton" data-invisible-in-mode='["reservedTableSelected", "activeTableSelected"]'><!--[lang:reserve_table]--></a> <a class="posButton reserveTableButton" data-invisible-in-mode='["reservedTableSelected", "activeTableSelected"]'><!--[lang:reserve_table]--></a>
<a class="posButton unreserveTableButton" data-visible-in-mode='["reservedTableSelected"]'><!--[lang:unreserve_table]--></a> <a class="posButton unreserveTableButton" data-visible-in-mode='["reservedTableSelected"]'><!--[lang:unreserve_table]--></a>
<a class="posButton payTableButton" data-visible-in-mode='["activeTableSelected"]'><!--[lang:pay_table]--></a> <a class="posButton payTableButton" data-visible-in-mode='["activeTableSelected"]'><!--[lang:pay_table]--></a>

View File

@@ -2,60 +2,69 @@
<html> <html>
<head> <head>
<title>DredgePOS</title> <title>DredgePOS</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://unpkg.com/current-device/umd/current-device.min.js"></script>
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name = "viewport" content = "user-scalable = no ,shrink-to-fit=yes" /> <meta name = "viewport" content = "user-scalable = no ,shrink-to-fit=yes" />
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
</head> </head>
<body> <body>
<div id="pageContainer"> <div id="pageContainer" <!--[var: containerAttributes]-->>
<div id="leftColumn"> <div id="leftColumn">
<h1 class="tableHeading"><!--[lang:active_table]--></h1> <h1 class="tableHeading"><!--[var: orderNumber]--></h1>
<div class="tableInfo"> <div class="tableInfo">
<a href="#" class="posButton"><!--[lang:covers]--></a> <!--[var: changeCoverNumberButton]-->
<a class="posHeader">Logged in as <!--[arr:clerk|clerk_name]--></a></a> <a class="posButton">Logged in as <!--[arr:clerk|clerk_name]--></a>
</div> </div>
<div class="orderBox"> <div class="orderBox">
<table class="orderBoxTable">
<thead>
<tr>
<th class="orderBoxCell qtyCell"><!--[lang:qty_header]--></th>
<th class="orderBoxCell itemIdCell hidden"><!--[lang:id_header]--></th>
<th class="orderBoxCell itemCell"><!--[lang:item_header]--></th>
<th class="orderBoxCell unitPriceCell hidden"><!--[lang:price_header]--></th>
<th class="orderBoxCell totalPriceCell"><!--[lang:total_price_header]--></th>
<th class="orderBoxCell printGroupCell"><!--[lang:printgroup_header]--></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="orderBoxInfo">
<span class="voidModeWarning" data-visible-in-mode='["void"]'><!--[lang:void_mode]--></span>
</div> </div>
<div class="orderBoxInfo"></div>
<div class="orderBoxFooter"> <div class="orderBoxFooter">
<span class="orderBoxTotal"><!--[lang:totalPrice|0.00]--></span>
<small class="orderBoxSelectedTotal"><!--[lang:selectedPrice|0.00]--></small>
</div> </div>
</div> </div>
<div id="rightColumn"> <div id="rightColumn">
<div id="topHalf"> <div id="topHalf">
<div class="utilityButtons">
<a class="posButton"></a>
<a class="posButton"></a>
<a class="posButton"></a>
<a class="posButton logoutButton">×</a>
</div>
<div class="functionButtons"> <div class="functionButtons">
<div class="functionColumn"> <div class="printGroupButtons toggleGroup">
<a class="posButton"></a> <input type="hidden" name="print_override" class="value" />
<a class="posButton"></a> <!--[var:salesCategoryOverrideButtons]-->
<a class="posButton"></a>
<a class="posButton"></a>
<a class="posButton"></a>
<a class="posButton"></a>
</div> </div>
<div class="functionColumn"> <div class="functionColumn">
<a class="posButton"></a> <a class="posButton accumulateButton" data-active-in-mode="accumulate"><!--[lang:accumulate_function]--></a>
<a class="posButton"></a> <a class="showCoverSelectorButton posButton"><!--[lang:select_covers]--></a>
<a class="posButton voidButton"><!--[lang:void]--></a> </div>
<a class="posButton"></a> <div class="functionColumn">
<a class="posButton voidButton" data-active-in-mode="void"><!--[lang:void]--></a>
<a class="posButton openItemButton"><!--[lang:custom_item_button]--></a>
<a class="freetextButton posButton"><!--[lang:freetext_button]--></a>
<a class="numpadButton posButton"><!--[lang:numpad_button]--></a>
</div>
<div class="functionColumn">
<a class="posButton"><!--[lang:pay_function]--></a>
<a class="posButton"><!--[lang:print_function]--></a>
</div> </div>
<div class="functionColumn"></div>
<div class="functionColumn"></div>
</div> </div>
</div> </div>
<div id="pageList"> <div id="pageList">
<!--[var:categoryList]--> <!--[var:categoryList]-->
</div> </div>
<div id="pageContainer"> <div id="pageGroupContainer">
<!--[var:pageGroups]--> <!--[var:pageGroups]-->
</div> </div>
<div class="pageNavigation"> <div class="pageNavigation">
@@ -66,6 +75,10 @@
</div> </div>
<!--[template:keyboards]--> <!--[template:keyboards]-->
<!--[template:orderScreen/grid_container]-->
<!--[template:orderScreen/cover_selector]-->
<template id="posButtonTemplate">
<!--[template:components/posButton]-->
</template>
</body> </body>
</html> </html>

View File

@@ -1 +1 @@
<span class="buttonImg" style="background-image:url(images/items/<!--[var:image]-->);"></span> <span class="buttonImg" style="background-image:url(/images/items/<!--[var:image]-->);"></span>

View File

@@ -0,0 +1 @@
<a href="#" class="posButton changeCoverNumberButton"><!--[var:covers]--></a>

View File

@@ -0,0 +1,3 @@
<div class="coverSelector">
<!--[var:coverSelectorButtons]-->
</div>

View File

@@ -0,0 +1,13 @@
<div class="gridContainer">
<div class="gridContainerHeader">
<span></span>
<div class="posButton closeGrid">×</div>
</div>
<div class="gridContainerGrid">
<div class="pageGroup"></div>
</div>
<div class="pageNavigation">
<a class="posButton prevButton"><!--[lang:prev_page]--></a>
<a class="posButton nextButton"><!--[lang:next_page]--></a>
</div>
</div>

View File

@@ -1,3 +1,6 @@
<div class="gridPage"> <div class="gridPage" style="
grid-template-columns: repeat(<!--[var:cols]-->, 1fr);
grid-template-rows: repeat(<!--[var:rows]-->, 1fr);
">
<!--[var:pageButtons]--> <!--[var:pageButtons]-->
</div> </div>