50 Commits

Author SHA1 Message Date
9698671de7 Preparing for reactivity 2022-07-07 18:04:39 +10:00
dredgy
f133284309 Merge pull request #11 from dredgy/make_order_screen_reactive
Light fixes
2022-07-07 15:56:49 +10:00
dredgy
9558074c4f Slight refactoring to button action attributes 2022-07-07 15:42:40 +10:00
dredgy
ac30d8147e Few updates to cover selector width calculation 2022-07-07 14:48:32 +10:00
dredgy
b9a0b578e3 Updated typescript 2022-07-06 14:28:55 +10:00
dredgy
0a2ffa123a Merge pull request #10 from dredgy/test_data_update
instruction row now hides price if $0
2022-07-03 19:27:44 +10:00
510bd8cfb9 instruction row now hides price if $0 2022-07-03 19:09:31 +10:00
dredgy
b33db1987b Merge pull request #9 from dredgy/test_data_update
Migration now orders pages correctly, adds demo floorplan. Some small…
2022-07-02 23:29:21 +10:00
aa37b24293 Migration now orders pages correctly, adds demo floorplan. Some small fixes on order screen. 2022-07-02 23:28:42 +10:00
dredgy
627843f3a3 Merge pull request #8 from dredgy/orderscreen_view
Orderscreen view
2022-07-02 14:23:28 +10:00
6c7bb9eff4 Order Screen fully ported to Giraffe View Engine 2022-07-02 14:22:52 +10:00
905adcd7bd Migration test data complete 2022-07-01 15:25:10 +10:00
5e78701b0b Buttons now render entirely in Giraffe 2022-06-30 23:43:16 +10:00
420c6530e0 Lost source code, switching back to main branch 2022-06-29 22:03:45 +10:00
100a772297 Updates to Order Screen 2022-05-12 20:03:39 +10:00
a587423d3e view progression 2022-03-07 21:36:38 +10:00
cc7d06e78b Change to background image loader 2022-02-27 11:57:52 +10:00
05a1a71e6b CSS background in quotes 2022-02-26 22:31:59 +10:00
dredgy
b6aadc072f Merge pull request #7 from dredgy/installer
Migration system added.
2022-02-26 22:24:07 +10:00
207edf0de3 Migration system added.
Install scripts for database schema and dummy data too.
2022-02-26 22:23:30 +10:00
dredgy
6439b4326c Merge pull request #6 from dredgy/add_view_engine
Moved to Giraffe View Engine
2022-02-22 15:23:22 +10:00
cb58262507 Moved to Giraffe View Engine 2022-02-22 15:22:32 +10:00
dredgy
b5a2514495 Merge pull request #5 from dredgy/npm_scripts
Npm scripts
2022-02-14 19:04:54 +10:00
a1d3ad19c3 Updated gitignore 2022-02-14 16:33:03 +10:00
300fb23905 Updated gitignore 2022-02-14 16:32:11 +10:00
6892b3d34c Restructured files, made build script 2022-02-14 16:31:08 +10:00
c73184808c Added solution 2022-02-14 16:06:08 +10:00
dredgy
c7f3d7d2fb Merge pull request #4 from dredgy/project-structure
Project structure
2022-02-13 19:35:26 +10:00
e3794fc222 Unversioned file was tracking 2022-02-13 19:35:02 +10:00
39dbba45f7 Separated models, controllers and routers 2022-02-13 19:33:50 +10:00
dredgy
6cbd670260 Merge pull request #3 from dredgy/order_screen
Join With Line function
2022-02-13 13:10:24 +10:00
430ad87880 Join With Line function 2022-02-13 13:09:53 +10:00
dredgy
abfb2331ba Merge pull request #2 from dredgy/order_screen
Order screen
2022-01-23 18:18:06 +10:00
445cbbabe5 OrderScreen complete 2022-01-23 18:16:18 +10:00
75c129c5d4 Updates to scaling on phones 2022-01-11 06:51:55 +10:00
56d42f1339 Added open food/freetext commands 2022-01-08 19:25:01 +10:00
f656c5ab40 Added print group overrides 2022-01-08 17:53:23 +10:00
198d609e62 Added mutation observer to sum totals 2022-01-07 10:41:49 +10:00
fc4a5d8624 Finalization of order box behavior 2022-01-06 12:01:09 +10:00
30bc79ef98 Finished void functionality 2022-01-05 14:54:40 +10:00
85722fa692 Updates to order screen 2022-01-04 15:04:55 +10:00
4de8f20d00 Fixed data-item attribute 2022-01-02 20:05:21 +10:00
3067bd1bfc Repair 2022-01-02 16:17:18 +10:00
21cd4b2018 Minor syntax 2022-01-02 15:19:10 +10:00
cbd157d2c9 Updated to F# 6 arrays 2022-01-02 15:17:36 +10:00
bb5e613382 Images 2022-01-01 21:58:20 +10:00
969e6b1b87 Converted from MySQL to PgSQL 2021-11-28 16:50:58 +10:00
db1b620aec images 2021-11-22 10:05:32 +10:00
a48c3a68e0 Added Basic Order Screen 2021-11-21 20:44:44 +10:00
dd1ed266f8 Updates 2021-11-12 21:25:04 +10:00
199 changed files with 4677 additions and 2356 deletions

7
.gitignore vendored
View File

@@ -6,5 +6,8 @@
/tables/* /tables/*
/Properties/ /Properties/
/.idea/ /.idea/
/wwwroot/scripts/js/ /wwwroot/scripts/*.js
/wwwroot/styles/css/* /wwwroot/scripts/*.js.map
/wwwroot/styles/*
/Folder.DotSettings.user
sql.log

15
Ajax/Controller.fs Normal file
View File

@@ -0,0 +1,15 @@
module DredgePos.Ajax.Controller
open DredgeFramework
open language
open Giraffe
let getLanguageVars = ajaxSuccess languageVars
let getKeyboardLayout (language: string) =
let layout = $"""wwwroot/languages/{language}/keyboardLayout.json"""
|> GetFileContents
map [
"status", "success"
"data", layout
] |> json

14
Ajax/Router.fs Normal file
View File

@@ -0,0 +1,14 @@
module DredgePos.Ajax.Router
open Saturn
open Giraffe
let pipeline = pipeline {
use_warbler
}
let router = router {
getf "/getKeyboardLayout/%s" Controller.getKeyboardLayout
get "/languageVars" (json Controller.getLanguageVars)
}

View File

@@ -1,148 +0,0 @@
module AjaxController
open DredgeFramework
open DredgePos
open Floorplan
open Microsoft.AspNetCore.Http
open Reservations
open language
open Giraffe
open Types
let loginWithLoginCode (context: HttpContext) (login_code: int) =
if Session.clerkLogin login_code context then ajaxSuccess "success"
else ajaxFail "fail"
let getLanguageVars = ajaxSuccess languageVars
let getActiveTables venue = Floorplan.getActiveTables venue |> ajaxSuccess |> json
let getRoomData roomId = Floorplan.getRoom roomId |> ajaxSuccess |> json
let mergeTables (tables: floorplan_table[]) =
let status =
if mergeTables tables.[0].table_number tables.[1].table_number then
let outputTables = map [
"parent", tables.[0];
"child", tables.[1];
"merged", getTable tables.[0].table_number;
]
ajaxSuccess outputTables
else ajaxFail "Could Not Merge Tables"
status |> json
let unmergeTable tableNumber =
let unmerged = Floorplan.unmergeTable tableNumber
let unmergedTables =
match unmerged with
| Some (parent, child) ->
map["parent", parent; "child", child] |> ajaxSuccess
| None -> ajaxFail "Could not Unmerge Table"
unmergedTables |> json
let getFloorplanData venue =
let tableList = Floorplan.tableList venue
let reservationList = getReservationList tableList
{|
tables = tableList
decorations = Decorations.decorationList venue
activeTableNumbers = Floorplan.getActiveTables venue
rooms = Floorplan.getRoomList venue
reservations = reservationList
|}
|> ajaxSuccess
|> json
let getKeyboardLayout (language: string) =
let layout = $"""wwwroot/languages/{language}/keyboardLayout.json""" |> GetFileContents
map [
"status", "success"
"data", layout
] |> json
let getRoomTablesAndDecorations roomId =
let tables = Floorplan.tablesInRoom roomId
let decorations = Decorations.decorationsInRoom roomId
let data = {|
tables = tables
decorations = decorations
|}
data |> ajaxSuccess |> json
let getTableData tableNumber = json <| Floorplan.getTable tableNumber
let updateTableShape (table: floorplan_table) =
Floorplan.updateTableShape table |> ignore
getTableData table.table_number
let transformTable (table: floorplan_table) =
Floorplan.updateTablePosition table |> ignore
getTableData table.table_number
let createTable (tableData: floorplan_table) =
let result =
if tableExists tableData.table_number = "False" then
ajaxSuccess (addNewTable tableData)
else ajaxFail (tableExists tableData.table_number)
result |> json
let deleteTable (table: floorplan_table) =
Floorplan.deleteTable table.table_number
table |> ajaxSuccess |> json
let transferTable (origin, destination) =
Floorplan.transferTable origin destination
let data = map ["origin", getTable origin ; "destination", getTable destination]
ajaxSuccess data |> json
let AddDecoration (data: floorplan_decoration) =
let image = "wwwroot/images/decorations/" + data.decoration_image
let width, height = image |> GetImageSize
let aspectRatio = decimal width / decimal height
let decoration : floorplan_decoration = {
id = 0
decoration_height = (200m / aspectRatio) |> int
decoration_width = 200
decoration_rotation = 0
decoration_image = data.decoration_image
decoration_pos_x = data.decoration_pos_x
decoration_pos_y = data.decoration_pos_y
decoration_room = data.decoration_room
}
Decorations.CreateDecoration decoration |> ajaxSuccess |> json
let UpdateDecoration data =
Decorations.UpdateDecoration data |> ignore
ajaxSuccess "true" |> json
let DeleteDecoration id = ajaxSuccess (Decorations.DeleteDecoration id) |> json
let newEmptyReservation (reservation: reservation) =
let newReservation = {reservation with
reservation_created_at = CurrentTime()
reservation_time = CurrentTime()
}
if reservation.reservation_table_id > 0 then
let table = {(getTableById reservation.reservation_table_id) with
status = 2
default_covers = reservation.reservation_covers}
updateTablePosition table |> ignore
let createdReservation = Floorplan.createEmptyReservation newReservation
ajaxSuccess createdReservation |> json
let updateReservation (reservation: reservation) = updateReservation reservation |> ajaxSuccess |> json
let unreserveTable (table: floorplan_table) =
let newTable = {table with status = 0}
updateTablePosition newTable |> ignore
DeleteReservation newTable.id
newTable |> ajaxSuccess |> json

View File

@@ -0,0 +1,16 @@
module DredgePos.Authenticate.Controller
open DredgeFramework
open Microsoft.AspNetCore.Http
open DredgePos.Global.Controller
let loadAuthenticatePage =
let scripts = [|"dredgepos.authenticate.js"|] |> addDefaultScripts
let styles = [|"dredgepos.authenticate.css"|] |> addDefaultStyles
let metaTags = [|"viewport", "user-scalable = no, initial-scale=0.8,maximum-scale=0.8 ,shrink-to-fit=yes"|] |> addDefaultMetaTags
View.index scripts styles metaTags
let loginWithLoginCode (context: HttpContext) (login_code: int) =
if Model.clerkLogin login_code context then ajaxSuccess "success"
else ajaxFail "fail"

View File

@@ -1,30 +1,44 @@
module Session module DredgePos.Authenticate.Model
open System open System
open DredgeFramework open DredgeFramework
open Dapper.FSharp open Dapper.FSharp
open Clerk
open DredgePos open DredgePos
open Thoth.Json.Net open Thoth.Json.Net
open Types open Types
let getClerkByLoginCode (loginCode: int) =
let clerk =
select {
table "clerks"
where (eq "login_code" loginCode)
take 1
}
|> Database.Select<clerk>
|> EnumerableToArray
if (clerk |> length) > 0 then
Some (first clerk)
else
None
let deleteSession sessionId context = let deleteSession sessionId context =
delete { delete {
table "sessions" table "sessions"
where (eq "session_id" sessionId) where (eq "session_id" sessionId)
} |> db.Delete |> ignore } |> Database.Delete |> ignore
Browser.deleteCookie "dredgepos_clerk_logged_in" context Browser.deleteCookie "dredgepos_clerk_logged_in" context
let deleteSessionByClerkId clerk_id context = let deleteSessionByClerkId clerk_id context =
delete { delete {
table "sessions" table "sessions"
where (eq "clerk_id" clerk_id) where (eq "clerk_id" clerk_id)
} |> db.Delete |> ignore } |> Database.Delete |> ignore
Browser.deleteCookie "dredgepos_clerk_logged_in" context Browser.deleteCookie "dredgepos_clerk_logged_in" context
let createNewSession (clerk: clerk) context = let createNewSession (clerk: clerk) context =
if (getClerkByLoginCode clerk.clerk_login_code).IsSome then if (getClerkByLoginCode clerk.login_code).IsSome then
deleteSessionByClerkId clerk.id context deleteSessionByClerkId clerk.id context
let newSessionId = (Guid.NewGuid().ToString "N") + (Guid.NewGuid().ToString "N") let newSessionId = (Guid.NewGuid().ToString "N") + (Guid.NewGuid().ToString "N")
@@ -39,18 +53,14 @@ let createNewSession (clerk: clerk) context =
table "sessions" table "sessions"
value newSession value newSession
} }
|> db.Insert |> Database.Insert
|> ignore |> ignore
Browser.setCookie "dredgepos_clerk_logged_in" newSessionId (DateTimeOffset.UtcNow.AddHours(24.0)) context Browser.setCookie "dredgepos_clerk_logged_in" newSessionId (DateTimeOffset.UtcNow.AddHours(24.0)) context
let sessionExists (sessionId: string) context = let sessionExists (sessionId: string) context =
let sessions = let sessions = Entity.GetAllByColumn<session> "session_id" sessionId
select {
table "sessions"
where (eq "session_id" sessionId)
} |> db.Select<session>
match sessions |> length with match sessions |> length with
| 0 -> false | 0 -> false
@@ -64,13 +74,11 @@ let sessionExists (sessionId: string) context =
false false
let checkAuthentication clerk = let checkAuthentication clerk =
let existingClerk = getClerkByLoginCode clerk.clerk_login_code let existingClerk = getClerkByLoginCode clerk.login_code
if existingClerk.IsSome existingClerk.IsSome
&& existingClerk.Value.id = clerk.id && existingClerk.Value.id = clerk.id
&& existingClerk.Value.clerk_name = clerk.clerk_name && existingClerk.Value.name = clerk.name
&& existingClerk.Value.clerk_login_code = clerk.clerk_login_code && existingClerk.Value.login_code = clerk.login_code
then true
else false
let getLoginCookie context = Browser.getCookie "dredgepos_clerk_logged_in" context let getLoginCookie context = Browser.getCookie "dredgepos_clerk_logged_in" context
@@ -79,7 +87,7 @@ let getSession (sessionId: string) =
select { select {
table "sessions" table "sessions"
where (eq "session_id" sessionId) where (eq "session_id" sessionId)
} |> db.Select<session> } |> Database.Select<session>
match sessions |> length with match sessions |> length with
| 0 -> {session_id = ""; clerk_json = ""; clerk_id= 0; expires= 0; id=0} | 0 -> {session_id = ""; clerk_json = ""; clerk_id= 0; expires= 0; id=0}
@@ -87,7 +95,7 @@ let getSession (sessionId: string) =
let getCurrentClerk context = let getCurrentClerk context =
let cookie = getLoginCookie context let cookie = getLoginCookie context
let emptyClerk = {id=0; clerk_login_code=0; clerk_usergroup=0; clerk_name=""} let emptyClerk = {id=0; login_code=0; user_group_id=0; name=""}
match cookie with match cookie with
| "" -> | "" ->
Browser.redirect "/login" context Browser.redirect "/login" context

18
Authenticate/Router.fs Normal file
View File

@@ -0,0 +1,18 @@
module DredgePos.Authenticate.Router
open Saturn
open Giraffe
let homepage = (warbler (fun _ -> htmlView Controller.loadAuthenticatePage ))
let handlePostRoute<'a> handlerFunction post next ctx = json (handlerFunction ctx post) next ctx
let pipeline = pipeline{
use_warbler
}
let router = router {
pipe_through pipeline
get "/" homepage
get "" homepage
post "/authenticateClerk" (bindJson<int> (handlePostRoute Controller.loginWithLoginCode) )
}

10
Authenticate/View.fs Normal file
View File

@@ -0,0 +1,10 @@
module DredgePos.Authenticate.View
open DredgePos.Global.View
open Giraffe.ViewEngine
let content = div [_id "authenticator"] []
let index scripts styles metaTags = HtmlPage "Floorplan" (GetScripts scripts) (GetStyles styles) (GetMetaTags metaTags) [|content|]

View File

@@ -1,50 +0,0 @@
module Browser
open System
open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Routing
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open DredgeFramework
let cookieExists name (context: HttpContext) =
context.Request.Cookies.ContainsKey(name)
let deleteCookie name (context: HttpContext) =
if cookieExists name context then context.Response.Cookies.Delete(name)
let getCookie cookieName (context: HttpContext) =
context.Request.Cookies.[cookieName] |? ""
let setCookie name value (expiry: DateTimeOffset) (context: HttpContext) =
deleteCookie name context
let options = CookieOptions()
options.Expires <- expiry
context.Response.Cookies.Append(name, value, options);
let redirect url (context: HttpContext) =
context.Response.Redirect url
let addRoute path controller (endpoints: IEndpointRouteBuilder) =
endpoints.MapGet(path, fun context ->
context.Response.WriteAsync(controller())) |> ignore
endpoints
let addRouteWithParameter path controller param1 (endpoints: IEndpointRouteBuilder) =
endpoints.MapGet(path, fun context ->
let param1Name, param1Type = param1
let parameter1 = context.Request.RouteValues.[param1Name] |> string |> param1Type
context.Response.WriteAsync(controller parameter1)) |> ignore
endpoints
let addRouteWithParameters path controller param1 param2 (endpoints: IEndpointRouteBuilder) =
endpoints.MapGet(path, fun context ->
let param1Name, param1Type = param1
let param2Name, param2Type = param2
let parameter1 = context.Request.RouteValues.[param1Name] |> string |> param1Type
let parameter2 = context.Request.RouteValues.[param2Name] |> string |> param2Type
context.Response.WriteAsync(controller parameter1 parameter2)) |> ignore
endpoints

View File

@@ -1,37 +0,0 @@
module Clerk
open Dapper.FSharp
open DredgeFramework
open DredgePos
open Thoth.Json.Net
open Types
let mutable loginCookie = ""
let clerk_decoder : Decoder<clerk> =
Decode.object
(fun get ->
{
id = get.Required.Field "clerk_id" Decode.int
clerk_name = get.Required.Field "clerk_name" Decode.string
clerk_login_code = get.Required.Field "clerk_login_code" Decode.int
clerk_usergroup = get.Required.Field "clerk_usergroup" Decode.int
})
type user = {clerk_name:string}
let getClerkByLoginCode (loginCode: int) =
let clerk =
select {
table "clerks"
where (eq "clerk_login_code" loginCode)
take 1
}
|> db.Select<clerk>
|> EnumerableToArray
if (clerk |> length) > 0 then
Some (first clerk)
else
None

28
Core/Browser.module.fs Normal file
View File

@@ -0,0 +1,28 @@
module Browser
open System
open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Routing
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open DredgeFramework
let cookieExists name (context: HttpContext) =
context.Request.Cookies.ContainsKey(name)
let deleteCookie name (context: HttpContext) =
if cookieExists name context then context.Response.Cookies.Delete(name)
let getCookie cookieName (context: HttpContext) =
context.Request.Cookies[cookieName] |? ""
let setCookie name value (expiry: DateTimeOffset) (context: HttpContext) =
deleteCookie name context
let options = CookieOptions()
options.Expires <- expiry
context.Response.Cookies.Append(name, value, options);
let redirect url (context: HttpContext) = context.Response.Redirect url

98
Core/Database.module.fs Normal file
View File

@@ -0,0 +1,98 @@
module Database
open Dapper
open Dapper.FSharp.PostgreSQL
open DredgeFramework
open DredgePos.Types
open Npgsql
let connect connectionString = new NpgsqlConnection(connectionString)
let getDatabaseSettings () = (getConfig ()).database
let getConnectionString () =
let db = getDatabaseSettings ()
$"Server={db.host};Port={db.port};User Id={db.username};Password={db.password};Database={db.db_name};Include Error Detail=true"
let connectToDatabase () = connect (getConnectionString ())
let closeAndReturn (connection: NpgsqlConnection) (result: 'a) =
connection.Dispose()
result
let Select<'a> asyncQuery =
let connection = connectToDatabase ()
asyncQuery
|> connection.SelectAsync<'a>
|> RunSynchronously
|> EnumerableToArray
|> closeAndReturn connection
let SelectJoin<'a, 'b> asyncQuery =
let connection = connectToDatabase ()
asyncQuery
|> connection.SelectAsync<'a, 'b>
|> RunSynchronously
|> EnumerableToArray
|> closeAndReturn connection
let Insert<'a> asyncQuery =
let connection = connectToDatabase ()
asyncQuery
|> connection.InsertAsync<'a>
|> RunSynchronously
|> closeAndReturn connection
let InsertOutput<'a> asyncQuery =
let connection = connectToDatabase ()
asyncQuery
|> connection.InsertOutputAsync<'a, 'a>
|> RunSynchronously
|> EnumerableToArray
|> closeAndReturn connection
let Update<'a> asyncQuery =
let connection = connectToDatabase ()
asyncQuery
|> connection.UpdateOutputAsync<'a, 'a>
|> RunSynchronously
|> EnumerableToArray
|> closeAndReturn connection
let Delete<'a> asyncQuery =
let connection = connectToDatabase ()
asyncQuery
|> connection.DeleteAsync
|> RunSynchronously
|> closeAndReturn connection
let NonDbSpecificQuery (sql: string) (connection: NpgsqlConnection) =
sql
|> fun str -> System.IO.File.WriteAllText("sql.log", str); str
|> connection.Execute
|> closeAndReturn connection
let rawQuery (sql: string) = connectToDatabase () |> NonDbSpecificQuery sql
let CreateTable (tableName: string) (columnList: (string * string) list) =
let columns =
columnList
|> List.filter (fun (columnName, _) -> columnName <> "id")
|> List.map (fun (columnName, columnType) -> $""" "{columnName}" {columnType} not null""")
|> String.concat ",\n\t\t\t"
$"""
create table if not exists {tableName}
(
id serial
constraint {tableName}_pk
primary key,
{columns}
);
"""
|> fun str -> System.IO.File.WriteAllText("sql.log", str); str
|> rawQuery
|> ignore

View File

@@ -2,20 +2,23 @@
open System.Collections.Generic open System.Collections.Generic
open System.Globalization open System.Globalization
open FSharp.Data.Sql
open System open System
open System.Drawing open System.Drawing
open System.IO open System.IO
open System.Linq open System.Linq
open System.Xml; open System.Xml;
open System.Xml.XPath;
open System.Xml.Xsl open System.Xml.Xsl
open DredgePos.Types
open FSharp.Reflection open FSharp.Reflection
open Thoth.Json.Net open Thoth.Json.Net
let (|?) lhs rhs = if lhs = null then rhs else lhs let (|?) lhs rhs = if lhs = null then rhs else lhs
let joinWithNewLine (arr: string[]) = arr |> String.concat "\n"
let getCurrentVenue () = 1
let map list = list |> Map.ofList let map list = list |> Map.ofList
let JoinArray (char: string) (array: 'a[]) = String.Join(char, array) let JoinArray (char: string) (array: 'a[]) = String.Join(char, array)
@@ -31,13 +34,16 @@ let EnumerableToArray (enumerable: IEnumerable<'T>) = enumerable.ToArray()
let getFileExtension (file: string) = Path.GetExtension file let getFileExtension (file: string) = Path.GetExtension file
let GetFileContents (file: string) = File.ReadAllText file let GetFileContents = File.ReadAllText
let GetFileName (file: string) = Path.GetFileName file let GetFileName (file: string) = Path.GetFileName file
let FileExists = File.Exists
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
@@ -95,3 +101,13 @@ let GetImageSize image =
loadedImage.Width, loadedImage.Height loadedImage.Width, loadedImage.Height
let CurrentTime() = DateTimeOffset.Now.ToUnixTimeSeconds() |> int let CurrentTime() = DateTimeOffset.Now.ToUnixTimeSeconds() |> int
let getConfig () =
"config.json"
|> GetFileContents
|> Decode.Auto.fromString<config>
|> (fun result ->
match result with
| Ok config -> config
| Error message -> failwith ("config.json is not valid :" + message)
)

View File

@@ -0,0 +1,79 @@
module Entity
open Dapper
open Dapper.FSharp
open DredgeFramework
open Pluralize.NET.Core
open FSharp.Reflection
let GetDatabaseTable<'x> =
let typeName = typeof<'x>.Name
Pluralizer().Pluralize typeName
let Create (record: 'x)=
let tableName = GetDatabaseTable<'x>
insert {
table tableName
value record
excludeColumn "id"
}
|> Database.InsertOutput
|> first
let inline Update (record: ^x) =
let tableName = GetDatabaseTable<'x>
let id = ((^x) : (member id : int) record)
update {
table tableName
set record
where (eq "id" id)
excludeColumn "id"
}
|> Database.Update
let GetAll<'x> =
let tableName = GetDatabaseTable<'x>
select {
table tableName
}
|> Database.Select<'x>
let GetAllByColumn<'x> (column: string) (value: obj) =
let tableName = GetDatabaseTable<'x>
select {
table tableName
where (eq column value)
} |> Database.Select<'x>
let GetFirstByColumn<'x> (column: string) (value: obj) = (GetAllByColumn<'x> column value) |> first
let GetAllInVenue<'x> = GetAllByColumn<'x> "venue_id" (getCurrentVenue ())
let GetById<'x> (id: int) = GetAllByColumn<'x> "id" id |> first
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 tableName = Pluralizer().Pluralize typeName
let entity = GetById<'x> id
delete {
table tableName
where (eq "id" id)
} |> Database.Delete |> ignore
entity
let inline Delete< ^x when ^x: (member id: int) > (entity: ^x) =
typeof<'x>.GetProperty("id").GetValue(entity) :?> int
|> DeleteById<'x>

View File

@@ -19,15 +19,15 @@ let languageVars =
//Gets a value of a language variable //Gets a value of a language variable
let get var = let get var =
if languageVars.ContainsKey var then if languageVars.ContainsKey var then
languageVars.[var] languageVars[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,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
@@ -10,14 +10,14 @@ open DredgeFramework
let currentTheme = "restaurant" let currentTheme = "restaurant"
let getHTMLForFile file = let getHTMLForFile file =
let stylePath = $"/styles/css/{file}" let stylePath = $"/styles/{file}"
let scriptPath = $"/scripts/js/{file}" let scriptPath = $"/scripts/{file}"
let fileExtension = file |> getFileExtension let fileExtension = file |> getFileExtension
let scriptFileExists = File.Exists ("wwwroot"+stylePath) || File.Exists("wwwroot"+scriptPath) let scriptFileExists = File.Exists ("wwwroot"+stylePath) || File.Exists("wwwroot"+scriptPath)
match scriptFileExists with match scriptFileExists with
| true -> | true ->
match fileExtension with match fileExtension with
| ".css" -> $"\t<link test rel=\"stylesheet\" href=\"{stylePath}\" />" | ".css" -> $"\t<link rel=\"stylesheet\" href=\"{stylePath}\" />"
| ".js" -> | ".js" ->
let snippet = $"\t<script src=\"{scriptPath}\"></script>" let snippet = $"\t<script src=\"{scriptPath}\"></script>"
snippet snippet
@@ -26,7 +26,7 @@ let getHTMLForFile file =
let ParseScriptsAndStylesheets files html = let ParseScriptsAndStylesheets files html =
let defaultScriptsAndStyles = ["dark.theme.css"; "external/jquery.js" ; "dredgepos.core.js"; "keyboards.js";] let defaultScriptsAndStyles = ["dark.theme.css"; "./external/jquery.js" ; "dredgepos.core.js"; "keyboards.js";]
let scriptsAndStylesheets = defaultScriptsAndStyles @ files let scriptsAndStylesheets = defaultScriptsAndStyles @ files
let scriptAndStylesheetHTML = let scriptAndStylesheetHTML =
@@ -43,24 +43,24 @@ let ParseVariables (varArray: Map<string, string>) (html:string) =
Regex.Replace(html, "<!--\[var\:(.*?)\]-->", Regex.Replace(html, "<!--\[var\:(.*?)\]-->",
MatchEvaluator( MatchEvaluator(
fun matchedVar -> fun matchedVar ->
let varName = matchedVar.Groups.[1] |> string |> StringTrim let varName = matchedVar.Groups[1] |> string |> StringTrim
if varArray.ContainsKey varName then if varArray.ContainsKey varName then
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) =
Regex.Replace(string, "<!--\[arr\:(.*?)\|(.*?)\]-->", Regex.Replace(string, "<!--\[arr\:(.*?)\|(.*?)\]-->",
MatchEvaluator( MatchEvaluator(
fun matchedVar -> fun matchedVar ->
let arrayName = matchedVar.Groups.[1].ToString() |> StringTrim let arrayName = matchedVar.Groups[1].ToString() |> StringTrim
let keyName = matchedVar.Groups.[2].ToString() let keyName = matchedVar.Groups[2].ToString()
if arrayArray.ContainsKey arrayName && arrayArray.[arrayName].ContainsKey keyName then if arrayArray.ContainsKey arrayName && arrayArray[arrayName].ContainsKey keyName then
arrayArray.[arrayName].[keyName] arrayArray[arrayName][keyName]
else else
"<!--[Undefined Array: " + arrayName + "]-->" "<!--[Undefined Array: " + arrayName + "]-->"
) )
@@ -70,7 +70,7 @@ let ParseSimpleLanguageVariables (string:string) =
Regex.Replace(string, "<!--\[lang\:(.*?)\]-->", Regex.Replace(string, "<!--\[lang\:(.*?)\]-->",
new MatchEvaluator( new MatchEvaluator(
fun matchedVar -> fun matchedVar ->
let varName = matchedVar.Groups.[1].ToString() let varName = matchedVar.Groups[1].ToString()
|> StringTrim |> StringTrim
language.get varName language.get varName
@@ -78,10 +78,10 @@ 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()
|> StringSplit "," |> StringSplit ","
|> Array.toList |> Array.toList
@@ -89,7 +89,7 @@ let ParseLanguageVariablesWithReplacements (string: string) =
)) ))
let getTemplateFilePath templateName = let getTemplateFilePath templateName =
"wwwroot/themes/"+ currentTheme + "/" + templateName + ".tpl.htm" "views/"+ currentTheme + "/" + templateName + ".tpl.htm"
let templateExists templateName = let templateExists templateName =
templateName templateName
@@ -115,13 +115,16 @@ let rec loadTemplateWithVarsArraysScriptsAndStyles templateName vars arrays scri
and ParseTemplates vars arrays scripts styles (string: string) = and ParseTemplates vars arrays scripts styles (string: string) =
Regex.Replace(string, "<!--\[template\:(.*?)\]-->", Regex.Replace(string, "<!--\[template\:(.*?)\]-->",
new MatchEvaluator( fun template -> new MatchEvaluator( fun template ->
let templateName = template.Groups.[1].ToString() |> StringTrim let templateName = template.Groups[1].ToString() |> StringTrim
loadTemplateWithVarsArraysScriptsAndStyles templateName vars arrays scripts styles loadTemplateWithVarsArraysScriptsAndStyles templateName vars arrays scripts styles
)) ))
let loadTemplate templateName = let loadTemplate templateName =
loadTemplateWithVarsArraysScriptsAndStyles templateName Map.empty<string, string> Map.empty<string, Map<string, string>> [] [] loadTemplateWithVarsArraysScriptsAndStyles templateName Map.empty<string, string> Map.empty<string, Map<string, string>> [] []
let loadTemplateWithArrays templateName arrays =
loadTemplateWithVarsArraysScriptsAndStyles templateName Map.empty arrays [] []
let loadTemplateWithVars templateName vars = let loadTemplateWithVars templateName vars =
loadTemplateWithVarsArraysScriptsAndStyles templateName vars Map.empty<string, Map<string, string>> [] [] loadTemplateWithVarsArraysScriptsAndStyles templateName vars Map.empty<string, Map<string, string>> [] []
@@ -135,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

134
Core/Types.fs Normal file
View File

@@ -0,0 +1,134 @@
module DredgePos.Types
[<CLIMutable>]
type reservation = {
id: int
name: string
time: int
covers: int
floorplan_table_id: int
created_at: int
}
[<CLIMutable>]
type venue = {
id: int
name: string
}
[<CLIMutable>]
type floorplan_table = {
table_number: int
room_id: int
venue_id: int
pos_x: int
pos_y: int
shape: string
width: int
height: int
default_covers: int
rotation: int
merged_children: string
previous_state: string
status: int
id: int
}
[<CLIMutable>]
type print_group = {
id: int
name: string
printer_id: int
venue_id: int
}
[<CLIMutable>]
type sales_category = {
id: int
parent: int
name: string
print_group_id: int
venue_id: int
}
[<CLIMutable>]
type room = {
id: int
name: string
background_image: string
venue_id: int
}
[<CLIMutable>]
type floorplan_decoration = {
id: int
room_id: int
pos_x: int
pos_y: int
rotation: int
width: int
height: int
image: string
venue_id: int
}
[<CLIMutable>]
type clerk = {
id: int
name: string
login_code: int
user_group_id: int
}
[<CLIMutable>]
type session = {id: int; session_id: string; clerk_json: string; clerk_id: int; expires: int}
[<CLIMutable>]
type order_screen_page_group = {id: int; order: int; venue_id: int; label: string; grid_id: int}
[<CLIMutable>]
type grid = {id: int; name: string; rows: int; cols: int; data: string}
[<CLIMutable>]
type button = {
id: int
text: string
primary_action: string
primary_action_value: string
secondary_action: string
secondary_action_value: string
image: string
extra_classes: string
extra_styles: string
}
[<CLIMutable>]
type item = {
id: int
code: string
sales_category_id: int
name: string
item_type: string
price1: int
}
[<CLIMutable>]
type db_config = {
db_name: string
username: string
password: string
host: string
port: int
}
[<CLIMutable>]
type config = {
database: db_config
}
[<CLIMutable>]
type migration = {
id: int
name: string
timestamp: int
}

View File

@@ -1,47 +0,0 @@
module db
open Dapper
open Dapper.FSharp
open Dapper.FSharp.MySQL
open MySql.Data.MySqlClient
open DredgeFramework
let connString = "server=localhost;uid=root;pwd=;database=dredgepos;table cache = false"
let connection = new MySqlConnection(connString)
let Select<'a> asyncQuery =
asyncQuery
|> connection.SelectAsync<'a>
|> RunSynchronously
|> EnumerableToArray
let SelectJoin<'a, 'b> asyncQuery =
asyncQuery
|> connection.SelectAsync<'a, 'b>
|> RunSynchronously
|> EnumerableToArray
let Insert<'a> asyncQuery =
asyncQuery
|> connection.InsertAsync<'a>
|> RunSynchronously
let InsertOutput<'a> asyncQuery =
asyncQuery
|> connection.InsertAsync<'a>
|> RunSynchronously
|> ignore
let table = asyncQuery.Table
connection.Query<'a>($"""Select * From {table} Where id = (select last_insert_id())""")
|> EnumerableToArray
let Update<'a> asyncQuery =
asyncQuery
|> connection.UpdateAsync<'a>
|> RunSynchronously
let Delete<'a> asyncQuery =
asyncQuery
|> connection.DeleteAsync
|> RunSynchronously

View File

@@ -1,86 +0,0 @@
module Decorations
open System
open System.IO
open System.Text.RegularExpressions
open DredgeFramework
open Dapper.FSharp
open DredgePos
open Types
let decorationList venue =
select {
table "floorplan_decorations"
innerJoin "floorplan_rooms" "id" "decoration_room"
}
|> db.SelectJoin<floorplan_decoration, floorplan_room>
|> Array.filter (fun (_, room) -> room.venue_id = venue )
|> Array.map fst
let decorationsInRoom (roomId: int) =
select {
table "floorplan_decorations"
where (eq "decoration_room" roomId)
}
|> db.Select<floorplan_decoration>
let getImageName (image: string, path: string) =
let imageName =
image
|> StringReplace "-" " "
|> StringReplace "_" " "
|> ToTitleCase
imageName, path
let isImageFile (fileName: string) = Regex.IsMatch(fileName |> ToLowerCase, @"^.+\.(jpg|jpeg|png|gif)$")
let getImageHTML (imageName: string, imageUrl: string) =
let vars = map [
"image_name", imageName
"image_url", imageUrl
]
Theme.loadTemplateWithVars "decoratorItem" vars
let GetFileNameWithoutExtension (path: string) =
let name = Path.GetFileNameWithoutExtension path
name, path |> Path.GetFileName
let getImageRowHtml (imagesInRow: string[]) =
let vars = map ["decorations", String.Join("", imagesInRow)]
Theme.loadTemplateWithVars "decoratorRow" vars
let generateDecorator () =
"wwwroot/images/decorations"
|> Directory.GetFiles
|> Array.filter isImageFile
|> Array.map GetFileNameWithoutExtension
|> Array.map getImageName
|> Array.map getImageHTML
|> Array.chunkBySize 4
|> Array.map getImageRowHtml
|> JoinArray ""
let CreateDecoration (decoration: floorplan_decoration) =
insert {
table "floorplan_decorations"
value decoration
}
|> db.InsertOutput
|> first
let UpdateDecoration (decoration: floorplan_decoration) =
update {
table "floorplan_decorations"
set decoration
where (eq "id" decoration.id )
} |> db.Update
let DeleteDecoration (decoration: floorplan_decoration) =
delete {
table "floorplan_decorations"
where (eq "id" decoration.id)
} |> db.Delete |> ignore
decoration

View File

@@ -5,20 +5,44 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Types.fs" /> <Compile Include="Core\Types.fs" />
<Compile Include="DredgeFramework.module.fs" /> <Compile Include="Core\DredgeFramework.module.fs" />
<Compile Include="Browser.module.fs" /> <Compile Include="Core\Browser.module.fs" />
<Compile Include="Database.module.fs" /> <Compile Include="Core\Database.module.fs" />
<Compile Include="GenericEntities.module.fs" /> <Compile Include="Core\GenericEntities.module.fs" />
<Compile Include="Language.module.fs" /> <Compile Include="Core\Language.module.fs" />
<Compile Include="Theme.module.fs" /> <Compile Include="Core\Theme.module.fs" />
<Compile Include="Reservations.module.fs" /> <Compile Include="Printer.module.fs" />
<Compile Include="Floorplan.module.fs" /> <Compile Include="Global\View.fs" />
<Compile Include="Decorations.module.fs" /> <Compile Include="Global\Controller.fs" />
<Compile Include="Clerk.module.fs" /> <Compile Include="Global\Router.fs" />
<Compile Include="Session.module.fs" /> <Compile Include="Entities\Floorplan_Decorations\Model.fs" />
<Compile Include="PageController.fs" /> <Compile Include="Entities\Floorplan_Decorations\View.fs" />
<Compile Include="AjaxController.fs" /> <Compile Include="Entities\Floorplan_Decorations\Controller.fs" />
<Compile Include="Entities\Floorplan_Decorations\Router.fs" />
<Compile Include="Entities\Buttons\Model.fs" />
<Compile Include="Authenticate\Model.fs" />
<Compile Include="Authenticate\View.fs" />
<Compile Include="Authenticate\Controller.fs" />
<Compile Include="Authenticate\Router.fs" />
<Compile Include="Ajax\Controller.fs" />
<Compile Include="Ajax\Router.fs" />
<Compile Include="Migrations\CreateDatabaseSchema.fs" />
<Compile Include="Migrations\PopulateTestData.fs" />
<Compile Include="Installer\Model.fs" />
<Compile Include="Installer\Controller.fs" />
<Compile Include="Installer\Router.fs" />
<Compile Include="Floorplan\Model.fs" />
<Compile Include="Floorplan\View.fs" />
<Compile Include="Floorplan\Controller.fs" />
<Compile Include="Floorplan\Router.fs" />
<Compile Include="OrderScreen\Model.fs" />
<Compile Include="OrderScreen\View.fs" />
<Compile Include="OrderScreen\Controller.fs" />
<Compile Include="OrderScreen\Router.fs" />
<Compile Include="Reservations\Model.fs" />
<Compile Include="Reservations\Controller.fs" />
<Compile Include="Reservations\Router.fs" />
<Compile Include="Program.fs" /> <Compile Include="Program.fs" />
</ItemGroup> </ItemGroup>
@@ -34,37 +58,87 @@
</Content> </Content>
<Content Remove="node_modules\**" /> <Content Remove="node_modules\**" />
<Content Remove="wwwroot\scripts\ts\test.ts" /> <Content Remove="wwwroot\scripts\ts\test.ts" />
<Content Remove="languages\**" />
<Content Include="sass\dark.theme.sass">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="sass\dredgepos.authenticate.sass">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="sass\dredgepos.core.sass">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="sass\dredgepos.floorplan.sass">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="sass\dredgepos.keyboards.sass">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="sass\dredgepos.orderScreen.sass">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="typescript\dredgepos.authenticate.ts">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="typescript\dredgepos.core.ts">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="typescript\dredgepos.floorplan.ts">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="typescript\dredgepos.orderScreen.ts">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="typescript\dredgepos.tables.ts">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="typescript\keyboards.ts">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="typescript\types.ts">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="typescript\typings\currency.d.ts">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="typescript\typings\konva.d.ts">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Folder Include="wwwroot\styles" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.0.78" /> <PackageReference Include="Dapper.FSharp" Version="2.4.1" />
<PackageReference Include="Dapper.Contrib" Version="2.0.78" /> <PackageReference Include="Giraffe" Version="6.0.0-alpha-2" />
<PackageReference Include="Dapper.FSharp" Version="1.16.0" /> <PackageReference Include="Npgsql" Version="6.0.0" />
<PackageReference Include="FSharp.Data" Version="4.0.1" /> <PackageReference Include="Pluralize.NET.Core" Version="1.0.0" />
<PackageReference Include="Saturn" Version="0.15.0-preview03" /> <PackageReference Include="Saturn" Version="0.15.0" />
<PackageReference Include="System.Data.SqlClient" Version="4.8.0" />
<PackageReference Include="FSharp.Data.SqlClient" Version="2.1.0-beta1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0-preview.2.21154.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0-preview.2.21154.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MySql.Data" Version="8.0.23" />
<PackageReference Include="MySql.Data.EntityFrameworkCore" Version="8.0.22" />
<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>
<Folder Include="languages\english" />
<Folder Include="tables\archived" /> <Folder Include="tables\archived" />
<Content Include="xslt\orderXMLtoHTML.xslt" /> <Content Include="xslt\orderXMLtoHTML.xslt" />
<Content Include="xslt\orderHtmltoXML.xslt" /> <Content Include="xslt\orderHtmltoXML.xslt" />
<Content Include="xslt\htmlToEscPos.xslt" /> <Content Include="xslt\htmlToEscPos.xslt" />
<Content Include=".gitignore" /> <Content Include=".gitignore" />
<Folder Include="wwwroot\scripts\js" />
<Folder Include="wwwroot\styles\css" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -375,6 +449,7 @@
<ItemGroup> <ItemGroup>
<EmbeddedResource Remove="node_modules\**" /> <EmbeddedResource Remove="node_modules\**" />
<EmbeddedResource Remove="languages\**" />
</ItemGroup> </ItemGroup>
</Project> </Project>

16
DredgePos.sln Normal file
View File

@@ -0,0 +1,16 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "DredgePos", "DredgePos.fsproj", "{3C52169C-8E40-472B-B87B-82F43F96BFB0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3C52169C-8E40-472B-B87B-82F43F96BFB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3C52169C-8E40-472B-B87B-82F43F96BFB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C52169C-8E40-472B-B87B-82F43F96BFB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3C52169C-8E40-472B-B87B-82F43F96BFB0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

18
Entities/Buttons/Model.fs Normal file
View File

@@ -0,0 +1,18 @@
module DredgePos.Entities.Buttons.Model
open DredgePos.Types
open DredgeFramework
let attr = Giraffe.ViewEngine.HtmlElements.attr
let getItemActionAttributes (itemCode: string) =
let item = Entity.GetFirstByColumn<item> "code" (StringTrim itemCode)
[item |> jsonEncode |> attr "data-item"]
let getGridActionAttributes (gridId: int) = [(attr "data-grid") <| jsonEncode gridId]
let getActionAttributes (action: string) (actionValue: string) =
match action with
| "item" -> getItemActionAttributes actionValue
| "grid" -> actionValue |> int |> getGridActionAttributes
| _ -> []

View File

@@ -0,0 +1,13 @@
module DredgePos.Entities.Floorplan_Decorations.Controller
open System.IO
let generateDecorator () =
"wwwroot/images/decorations"
|> Directory.GetFiles
|> Array.filter Model.isImageFile
|> Array.map Model.GetFileNameWithoutExtension
|> Array.map Model.getImageName
|> Array.map View.decoratorItem
|> Array.chunkBySize 4
|> Array.map View.decoratorRow

View File

@@ -0,0 +1,24 @@
module DredgePos.Entities.Floorplan_Decorations.Model
open System
open System.IO
open System.Text.RegularExpressions
open DredgeFramework
let decorationsInRoom (roomId: int) = Entity.GetAllByColumn "decoration_room" roomId
let getImageName (image: string, path: string) =
let imageName =
image
|> StringReplace "-" " "
|> StringReplace "_" " "
|> ToTitleCase
imageName, path
let isImageFile (fileName: string) = Regex.IsMatch(fileName |> ToLowerCase, @"^.+\.(jpg|jpeg|png|gif)$")
let GetFileNameWithoutExtension (path: string) =
let name = Path.GetFileNameWithoutExtension path
name, path |> Path.GetFileName

View File

@@ -0,0 +1,2 @@
module DredgePos.Entities.Floorplan_Decorations.Router

View File

@@ -0,0 +1,24 @@
module DredgePos.Entities.Floorplan_Decorations.View
open Giraffe.ViewEngine
open DredgePos.Global.View
let decoratorItem (imageName, imageUrl) =
let image = attr "data-image"
div [_class "decoratorItem"; image imageUrl] [
a [_style $"background-image:url('/images/decorations/{imageUrl}')"] []
a [] [str imageName]
]
let decoratorRow decoratorItems = div [_class "decoratorRow"] [yield! decoratorItems]
let decorator (decorationRows: XmlNode[]) =
div [_id "decorator"] [
div [_id "decoratorHeader"] [
h2 [] [lang "choose_decoration"]
a [_class "posButton hideDecorator"] [str "×"]
]
div [_id "decoratorContent"] [
yield! decorationRows
]
]

119
Floorplan/Controller.fs Normal file
View File

@@ -0,0 +1,119 @@
module DredgePos.Floorplan.Controller
open DredgeFramework
open DredgePos
open DredgePos.Global.Controller
open DredgePos.Entities
open DredgePos.Types
open Giraffe
open Microsoft.AspNetCore.Http
open Model
open System.IO
let makeRoomButton (room: room) =
let vars = map [
"roomId", room.id |> string
"roomName", room.name
]
Theme.loadTemplateWithVars "roomButton" vars
let getActiveTables venue = Model.getActiveTables venue |> ajaxSuccess |> json
let mergeTables (tables: floorplan_table[]) =
let status =
if mergeTables tables[0].table_number tables[1].table_number then
let outputTables = map [
"parent", tables[0];
"child", tables[1];
"merged", Model.getTable tables[0].table_number;
]
ajaxSuccess outputTables
else ajaxFail "Could Not Merge Tables"
status |> json
let unmergeTable tableNumber =
let unmerged = unmergeTable tableNumber
let unmergedTables =
match unmerged with
| Some (parent, child) ->
map ["parent", parent; "child", child] |> ajaxSuccess
| None -> ajaxFail "Could not Unmerge Table"
unmergedTables |> json
let getFloorplanData (id: int) =
let tableList = Entity.GetAllInVenue<floorplan_table>
let reservationList = getReservationList tableList
{|
tables = tableList
decorations = Entity.GetAllInVenue<floorplan_decoration>
activeTableNumbers = Model.getActiveTables (getCurrentVenue())
rooms = Entity.GetAllInVenue<room>
reservations = reservationList
|}
|> ajaxSuccess
|> json
let transformTable (table: floorplan_table) =
Entity.Update table
|> ajaxSuccess
|> json
let createTable (tableData: floorplan_table) =
if tableExists tableData.table_number = "False" then
ajaxSuccess (addNewTable tableData)
else ajaxFail (tableExists tableData.table_number)
|> json
let deleteTable (table: floorplan_table) =
Entity.DeleteById<floorplan_table> table.id
|> ignore
table |> ajaxSuccess |> json
let transferTable (origin, destination) =
transferTable origin destination
let data = map ["origin", getTable origin ; "destination", getTable destination]
ajaxSuccess data |> json
let AddDecoration (data: floorplan_decoration) =
let image = "wwwroot/images/decorations/" + data.image
let width, height = image |> GetImageSize
let aspectRatio = decimal width / decimal height
let decoration : floorplan_decoration = {
id = 0
height = (200m / aspectRatio) |> int
width = 200
rotation = 0
image = data.image
pos_x = data.pos_x
pos_y = data.pos_y
room_id = data.room_id
venue_id = data.venue_id
}
Entity.Create decoration
|> ajaxSuccess
|> json
let UpdateDecoration (data: floorplan_decoration) =
Entity.Update data
|> ignore
ajaxSuccess "true" |> json
let DeleteDecoration (decorationToDelete: floorplan_decoration) =
Entity.DeleteById<floorplan_decoration> decorationToDelete.id
|> ajaxSuccess
|> json
let loadFloorplanView (ctx: HttpContext) =
Authenticate.Model.RequireClerkAuthentication ctx
let roomMenu = Entity.GetAllInVenue<room> |> Array.map View.roomButton
let currentClerk = Authenticate.Model.getCurrentClerk ctx
let styles = [|"dredgepos.floorplan.css"|] |> addDefaultStyles
let scripts = [|"./external/konva.min.js" ; "dredgepos.floorplan.js"|] |> addDefaultScripts
let metaTags = [|"viewport", "user-scalable = no, initial-scale=0.8,maximum-scale=0.8 ,shrink-to-fit=yes"|] |> addDefaultMetaTags
View.index styles scripts metaTags currentClerk (Floorplan_Decorations.Controller.generateDecorator ()) roomMenu

View File

@@ -1,41 +1,12 @@
module Floorplan module DredgePos.Floorplan.Model
open DredgePos
open Reservations
let currentVenue = 1
open DredgePos.Types
open System open System
open System.IO open System.IO
open System.Xml.Linq open System.Xml.Linq
open DredgeFramework open DredgeFramework
open Dapper
open Dapper.FSharp open Dapper.FSharp
open Thoth.Json.Net open Thoth.Json.Net
open Types
let floorplan_table_decoder : Decoder<floorplan_table> =
Decode.object
(fun get ->
{
table_number = get.Required.Field "table_number" Decode.int
room_id = get.Required.Field "room_id" Decode.int
venue_id = get.Required.Field "venue_id" Decode.int
pos_x = get.Required.Field "pos_x" Decode.int
pos_y = get.Required.Field "pos_y" Decode.int
shape = get.Required.Field "shape" Decode.string
width = get.Required.Field "width" Decode.int
height = get.Required.Field "height" Decode.int
default_covers = get.Required.Field "default_covers" Decode.int
rotation = get.Required.Field "rotation" Decode.int
merged_children = get.Required.Field "merged_children" Decode.string
previous_state = get.Required.Field "previous_state" Decode.string
status = get.Required.Field "status" Decode.int
id = get.Required.Field "id" Decode.int
})
let activeTablePath = "tables/active/" let activeTablePath = "tables/active/"
let getTableFile (tableNumber: int) = let getTableFile (tableNumber: int) =
@@ -50,11 +21,11 @@ let tableIsOpen (table: floorplan_table) = tableNumberIsOpen table.table_number
let fileNameToTableNumber (fileName: string) = //Takes a file name for a floorplan table and returns the table number let fileNameToTableNumber (fileName: string) = //Takes a file name for a floorplan table and returns the table number
if fileName.Contains ".table" then if fileName.Contains ".table" then
let fileName = (fileName.Split ".").[0] let fileName = (fileName.Split ".")[0]
(fileName.Split "/table").[1] |> int (fileName.Split "/table")[1] |> int
else 0 else 0
let openTables = //Get a list of all open tables. let openTables () = //Get a list of all open tables.
let tableList = Directory.GetFiles(activeTablePath) let tableList = Directory.GetFiles(activeTablePath)
tableList tableList
@@ -68,7 +39,7 @@ let tablesInRoom (roomId: int) = //Get a list of all tables in a particular room
table "floorplan_tables" table "floorplan_tables"
where (eq "room_id" roomId) where (eq "room_id" roomId)
} }
|> db.Select<floorplan_table> |> Database.Select<floorplan_table>
let getActiveTables (venueId: int) = let getActiveTables (venueId: int) =
@@ -76,7 +47,7 @@ let getActiveTables (venueId: int) =
table "floorplan_tables" table "floorplan_tables"
where (eq "venue_id" venueId) where (eq "venue_id" venueId)
} }
|> db.Select |> Database.Select
|> Array.filter tableIsOpen |> Array.filter tableIsOpen
|> Array.map (fun table -> table.table_number) |> Array.map (fun table -> table.table_number)
@@ -116,70 +87,45 @@ let saveOrderToTable orderXML tableNumber =
File.WriteAllText(tableFile, tableXML) File.WriteAllText(tableFile, tableXML)
let getTable (tableNumber : int) = let getTableSafely (tableNumber: int) =
select { let query = select {
table "floorplan_tables" table "floorplan_tables"
where (eq "table_number" tableNumber + eq "venue_id" currentVenue) where (eq "table_number" tableNumber + eq "venue_id" (getCurrentVenue()))
} }
|> db.Select<floorplan_table>
|> first query
|> Database.Select<floorplan_table>
|> Array.tryItem 0
let getTable (tableNumber : int) =
match getTableSafely tableNumber with
| None -> failwith $"Table {tableNumber} not found in current venue"
| Some table -> table
let getTableById (id : int) = let getTableById (id : int) =
select { select {
table "floorplan_tables" table "floorplan_tables"
where (eq "id" id) where (eq "id" id)
} }
|> db.Select<floorplan_table> |> Database.Select<floorplan_table>
|> first |> first
let getRoom (roomId: int) = let getRoom (roomId: int) =
select { select {
table "floorplan_rooms" table "rooms"
where (eq "id" roomId) where (eq "id" roomId)
} |> db.Select<floorplan_room> |> first } |> Database.Select<room> |> first
let getRoomList (venueId: int) = let updateTablePosition (floorplanTable: floorplan_table) = Entity.Update floorplanTable
select {
table "floorplan_rooms"
where (eq "venue_id" venueId)
} |> db.Select<floorplan_room>
let updateFloorplanTable (tableNumber:int) (column: string) value =
//TODO: Make update query venue specific
let sql = "Update floorplan_tables Set @column = @value Where table_number = @tableNumber"
let parameters = [("column", box column); ("value", box value); ("tableNumber", box tableNumber)]
db.connection.Execute(sql, parameters) |> ignore
getTable tableNumber
let updateTableShape (floorplanTable: floorplan_table) =
update {
table "floorplan_tables"
set floorplanTable
where (eq "table_number" floorplanTable.table_number + eq "venue_id" currentVenue)
} |> db.Update
let updateTablePosition (floorplanTable: floorplan_table) =
update {
table "floorplan_tables"
set floorplanTable
where (eq "table_number" floorplanTable.table_number + eq "venue_id" currentVenue)
} |> db.Update
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 } |> Database.Update |> ignore
insert{
table "reservations"
value reservation
} |> db.InsertOutput |> first
Entity.Create reservation
let getChildTables tableNumber = let getChildTables tableNumber =
let table = getTable tableNumber let table = getTable tableNumber
@@ -213,15 +159,13 @@ let tableExists (tableNumber: int) =
let numberOfResults = let numberOfResults =
select{ select{
table "floorplan_tables" table "floorplan_tables"
where (eq "table_number" tableNumber + eq "venue_id" currentVenue) where (eq "table_number" tableNumber + eq "venue_id" (getCurrentVenue()))
} |> db.Select<floorplan_table> |> length } |> Database.Select<floorplan_table> |> length
match numberOfResults with match numberOfResults with
| 0 -> | 0 ->
let allTables = let allTables =
select { Entity.GetAllInVenue<floorplan_table>
table "floorplan_tables"
} |> db.Select<floorplan_table>
|> Array.map(findChildTable tableNumber) |> Array.map(findChildTable tableNumber)
|> Array.filter(fun tableNumber -> tableNumber <> 0) |> Array.filter(fun tableNumber -> tableNumber <> 0)
@@ -229,16 +173,16 @@ let tableExists (tableNumber: int) =
match allTables.Length with match allTables.Length with
| 0 -> false |> string //Table does not exist | 0 -> false |> string //Table does not exist
| _ -> | _ ->
let parentTableData = getTable allTables.[0] let parentTableData = getTable allTables[0]
let parentRoom = getRoom parentTableData.room_id let parentRoom = getRoom parentTableData.room_id
let parentRoomName = parentRoom.room_name let parentRoomName = parentRoom.name
language.getAndReplace "error_table_exists_merged" [parentRoomName; parentTableData.table_number.ToString()] language.getAndReplace "error_table_exists_merged" [parentRoomName; parentTableData.table_number.ToString()]
| _ -> | _ ->
let tableData = getTable tableNumber let tableData = getTable tableNumber
let room = getRoom tableData.room_id let room = getRoom tableData.room_id
language.getAndReplace "error_table_exists" [room.room_name] language.getAndReplace "error_table_exists" [room.name]
let addNewTableWithoutOutput (newTable: floorplan_table) = let addNewTableWithoutOutput (newTable: floorplan_table) =
@@ -246,23 +190,9 @@ let addNewTableWithoutOutput (newTable: floorplan_table) =
table "floorplan_tables" table "floorplan_tables"
value newTable value newTable
} }
|> db.Insert |> Database.Insert
let addNewTable (newTable: floorplan_table) = let addNewTable (newTable: floorplan_table) = Entity.Create newTable
let newTableList =
insert{
table "floorplan_tables"
value newTable
}
|> db.InsertOutput
newTableList |> first
let deleteTable (tableNumber: int) =
delete {
table "floorplan_tables"
where (eq "table_number" tableNumber + eq "venue_id" currentVenue)
} |> db.Delete |> ignore
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
@@ -292,13 +222,12 @@ let mergeTables parent child = //Merge two tables together
let existingChildrenJson = parentTable.merged_children |> StringTrim let existingChildrenJson = parentTable.merged_children |> StringTrim
let existingChildren = let existingChildren =
existingChildrenJson existingChildrenJson |> Decode.Auto.fromString<floorplan_table[]>
|> Decode.fromString(Decode.list floorplan_table_decoder)
let tableList = let tableList =
match existingChildren with match existingChildren with
| Error _ -> [newChildTable] | Error _ -> [|newChildTable|]
| Ok tables -> tables @ [newChildTable] | Ok tables -> [tables ; [|newChildTable|]] |> Array.concat
let newChildrenJson = tableList |> jsonEncode let newChildrenJson = tableList |> jsonEncode
let parentPreviousState = parentTable |> jsonEncode let parentPreviousState = parentTable |> jsonEncode
@@ -314,10 +243,11 @@ let mergeTables parent child = //Merge two tables together
pos_y = newPosY pos_y = newPosY
default_covers = parentTable.default_covers + childTable.default_covers default_covers = parentTable.default_covers + childTable.default_covers
|} |}
where (eq "table_number" parent + eq "venue_id" currentVenue) where (eq "table_number" parent + eq "venue_id" (getCurrentVenue()))
} |> db.Update |> ignore } |> Database.Update |> ignore
deleteTable child Entity.DeleteById<floorplan_table> newChildTable.id
|> ignore
true true
@@ -326,8 +256,8 @@ let updateUnmergedTables parentTable childTable =
update { update {
table "floorplan_tables" table "floorplan_tables"
set parentTable set parentTable
where(eq "table_number" parentTable.table_number + eq "venue_id" currentVenue) where(eq "table_number" parentTable.table_number + eq "venue_id" (getCurrentVenue()))
} |> db.Update |> ignore } |> Database.Update |> ignore
addNewTableWithoutOutput childTable |> ignore addNewTableWithoutOutput childTable |> ignore
true true
@@ -350,44 +280,22 @@ let unmergeTable tableNumber = //Separates a merged table into itself and the la
Some (getTable currentTable.table_number, unmergedChild) Some (getTable currentTable.table_number, unmergedChild)
| Error _ -> None | Error _ -> None
let convertRoomListToLinks (room: floorplan_room) =
let vars = map [
"roomId", room.id |> string
"roomName", room.room_name
]
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 -> table.id)
|> JoinArray ","
db.connection.Query<reservation>($"""Select * From reservations Where reservation_table_id In ({tableIds})""")
|> EnumerableToArray
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()
} }
insert { Entity.Create reservation
table "reservations"
value reservation
} |> db.Insert
let tableList venueId = let tableList () = Entity.GetAllInVenue<floorplan_table>
select{
table "floorplan_tables"
innerJoin "floorplan_rooms" "id" "floorplan_tables.room_id"
}
|> db.SelectJoin<floorplan_table, floorplan_room>
|> Array.filter (fun (_, room) -> room.venue_id = venueId )
|> Array.map fst

25
Floorplan/Router.fs Normal file
View File

@@ -0,0 +1,25 @@
module DredgePos.Floorplan.Router
open DredgePos
open DredgePos.Global.Router
open DredgePos.Types
open Saturn
open Giraffe
let floorplan = (htmlViewWithContext Controller.loadFloorplanView)
let router = router {
pipe_through Ajax.Router.pipeline
get "" floorplan
get "/" floorplan
post "/mergeTables" (bindJson<floorplan_table[]> Controller.mergeTables)
post "/transformTable" (bindJson<floorplan_table> Controller.transformTable)
post "/createTable" (bindJson<floorplan_table> Controller.createTable)
post "/addDecoration" (bindJson<floorplan_decoration> Controller.AddDecoration)
post "/updateDecoration" (bindJson<floorplan_decoration> Controller.UpdateDecoration)
post "/deleteDecoration" (bindJson<floorplan_decoration> Controller.DeleteDecoration)
post "/deleteTable" (bindJson<floorplan_table> Controller.deleteTable)
getf "/getFloorplanData/%i" Controller.getFloorplanData
getf "/transferTable/%i/%i" Controller.transferTable
getf "/unmergeTable/%i" Controller.unmergeTable
}

86
Floorplan/View.fs Normal file
View File

@@ -0,0 +1,86 @@
module DredgePos.Floorplan.View
open DredgePos.Global
open DredgePos.Global.View
open DredgePos.Entities
open DredgePos.Types
open Giraffe.ViewEngine
open DredgeFramework
let pageContainer (clerk: clerk) roomMenu =
let loggedInText = str (language.getAndReplace "logged_in_as" [clerk.name])
div [_id "pageContainer"] [
div [_id "floorplanLeftColumn"] [
div [_class "topCell"] [
a [_class "posHeader"] [loggedInText]
]
div [_class "middleCell"] []
div [_class "bottomCell"] []
]
div [_id "floorplanCenterColumn"] [
div [_class "topCell"] [
yield! roomMenu
]
div [_class "middleCell"] [
div [_id "floorplanCanvas"] []
div [_id "floorplanCanvas"] []
]
div [_class "bottomCell"] [
div [_class "editControls" ; VisibleInMode ["tableSelected"]] [
div [_class "posHeader currentTable"] [
b [_class "selectedTableNumber"] []
a [_class "reservationStatus"; VisibleInMode ["reservedTableSelected"]] []
small [_class "selectedTableCovers"] []
]
a [_class "posButton placeOrderButton"] [lang "order_table"]
a [_class "posButton reserveTableButton"; InvisibleInMode ["reservedTableSelected"; "activeTableSelected"]] [lang "reserve_table"]
a [_class "posButton unreserveTableButton"; VisibleInMode ["reservedTableSelected"]] [lang "unreserve_table"]
a [_class "posButton payTableButton"; VisibleInMode ["activeTableSelected"]] [lang "pay_table"]
a [_class "posButton viewTableButton"; VisibleInMode ["activeTableSelected"]] [lang "view_table"]
]
]
]
div [_id "floorplanRightColumn"] [
div [_class "topCell"] [
a [_class "posButton logOut"] [str "×"]
]
div [_class "middleCell"] [
a [_class "posButton editModeButton"] [lang "edit_floorplan"]
div [_class "floorplanControls useVisibility"; VisibleInMode ["edit"]] [
a [_class "posButton addTableButton"] [lang "add_table"]
a [_class "posButton addDecoration"] [lang "add_decoration"]
a [_class "posButton deleteDecoration useVisibility"; VisibleInMode ["decorationSelected"; "edit"] ] [lang "delete_decoration"]
a [_class "posButton deleteTableButton useVisibility"; VisibleInMode ["tableSelected"; "edit"]] [lang "delete_table"]
a [_class "posButton changeShapeButton useVisibility"; VisibleInMode ["tableSelected"; "edit"]] [lang "change_shape"]
]
div [_class "mergeControls useVisibility"; VisibleInMode ["tableSelected"]] [
a [_class "posButton mergeButton"; ActiveInMode "merge"] [lang "merge_table"]
a [_class "posButton unmergeButton"; VisibleInMode ["tableSelected"]] [lang "unmerge_table"]
a [_class "posButton transferTableButton" ; ActiveInMode "transfer" ; VisibleInMode ["activeTableSelected"]] [lang "transfer_table"]
]
]
div [_class "bottomCell"] []
]
]
let roomButton (room: room) = a [_class "posButton roomButton"; Value (string room.id)] [str room.name ]
let index styles scripts tags clerk decoratorRows roomMenu =
[|
pageContainer clerk roomMenu
decoratorRows |> Floorplan_Decorations.View.decorator
|]
|> HtmlPage "Floorplan" (GetScripts scripts) (GetStyles styles) (GetMetaTags tags)

View File

@@ -1,26 +0,0 @@
module Entity
open DredgePos
open Types
open Dapper.FSharp
open DredgeFramework
let getDatabaseTable (record: 'a) = record.GetType().ToString().ToLower() + "s"
let addToDatabase (record: 'x)=
let tableName = getDatabaseTable record
insert {
table tableName
value record
}
|> db.InsertOutput
|> first
let updateInDatabase (record: 'x) =
let tableName = getDatabaseTable record
(* Run an update query *)
update {
table tableName
set record
}
|> db.Update |> ignore
record

7
Global/Controller.fs Normal file
View File

@@ -0,0 +1,7 @@
module DredgePos.Global.Controller
open DredgeFramework
let addDefaultScripts scripts = scripts |> Array.append [|"./external/jquery.js" ; "dredgepos.core.js"; "keyboards.js";|]
let addDefaultStyles styles = styles |> Array.append [|"dark.theme.css";|]
let addDefaultMetaTags (tags: (string*string)[]) = tags |> Array.append [|"apple-mobile-web-app-capable", "yes"|]

18
Global/Router.fs Normal file
View File

@@ -0,0 +1,18 @@
module DredgePos.Global.Router
open Saturn
open Giraffe
let htmlViewWithContext func =
(fun ctx ->
func (snd ctx)
|> htmlView
)
|> warbler
let htmlViewWithContextAndId (id: int) func =
(fun ctx ->
func (snd ctx) id
|> htmlView
)
|> warbler

134
Global/View.fs Normal file
View File

@@ -0,0 +1,134 @@
module DredgePos.Global.View
open DredgeFramework
open DredgePos.Types
open Giraffe.ViewEngine
let Value = attr "data-value"
let _table (value: floorplan_table) = value |> jsonEncode |> (attr "data-table")
let VisibleInMode (value: string list) = value |> jsonEncode |> (attr "data-visible-in-mode")
let InvisibleInMode (value: string list) = value |> jsonEncode |> (attr "data-invisible-in-mode")
let ActiveInMode (value: string) = value |> (attr "data-active-in-mode")
let innerText = str
let lang key = language.get key |> str
let template = tag "template"
let scriptToHTML (scriptFile: string) =
let scriptPath = $"/scripts/{scriptFile}"
match FileExists ("wwwroot" + scriptPath) with
| true -> script [_src scriptPath] []
| false -> comment $"[Missing script: {scriptFile}]"
let GetScripts (scripts: string[]) = scripts |> Array.map scriptToHTML
let styleToHTML (stylesheet:string) =
let stylePath = $"/styles/{stylesheet}"
match FileExists ("wwwroot" + stylePath) with
| true -> link [_rel "stylesheet" ; _href stylePath]
| false -> comment $"[Missing style: {stylesheet}]"
let GetStyles (scripts: string[]) = scripts |> Array.map styleToHTML
let tagToHtml (tag, content) = meta [_name tag; _content content]
let GetMetaTags (tags: (string * string)[]) = tags |> Array.map tagToHtml
let VirtualKeyboardRow numberOfButtons =
let buttons = Array.init numberOfButtons (fun _ -> a [] [])
div [_class "virtualKeyboardRow"] [
yield! buttons
]
let VirtualKeyboard =
div [_id "virtualKeyboard"] [
div [_class "headingRow"] [
h3 [_id "virtualKeyboardHeading"] []
a [_class "posButton closeKeyboards"] [str "X"]
]
input [_type "text"; _name "virtualKeyboardInput"; _id "virtualKeyboardInput"]
div [_id "virtualKeyboardButtons"] [
VirtualKeyboardRow 13
VirtualKeyboardRow 14
VirtualKeyboardRow 13
VirtualKeyboardRow 11
VirtualKeyboardRow 1
]
span [_class "forceFocus"] []
]
let VirtualNumpadButton (text: string) =
a [_href "#"; Value (text.ToLower()); _class "posButton virtualNumpadButton"] [str text]
let VirtualNumpadRow (buttons:string[]) =
div [_class "virtualNumpadRow"] [
yield! Array.map VirtualNumpadButton buttons
]
let VirtualNumpad =
div [_id "virtualNumpad"] [
div [_class "headingRow"] [
h3 [_id "virtualNumpadHeading"] []
a [_class "posButton closeKeyboards"] [str "X"]
]
div [_id "virtualNumpadInput"] []
div [_id "virtualNumpadButtons"] [
VirtualNumpadRow [|"1"; "2"; "3"|]
VirtualNumpadRow [|"4"; "5"; "6"|]
VirtualNumpadRow [|"7"; "8"; "9"|]
VirtualNumpadRow [|"0"; "."; "Clear"|]
VirtualNumpadRow [|"Submit"|]
]
]
let alert =
div [_id "alert"] [
div [_id "alertHeading"] []
div [_id "alertMessage"] []
div [_id "alertButtons"] [
a [_class "posButton"; _id "alertOk"] [lang "alert_ok"]
a [_class "posButton"; _id "alertYes"] [lang "alert_yes"]
a [_class "posButton"; _id "alertNo"] [lang "alert_no"]
]
]
let keyboards = [|
VirtualKeyboard
VirtualNumpad
alert
|]
let posButton (extraClasses: string) attrs content =
let allAttrs = [_class $"posButton {extraClasses}"] |> List.append attrs
a allAttrs content
let PosButton classes (attrs: Map<string, 'x>) text =
let attrArray =
attrs
|> Map.map (fun key value ->
(attr key) (string value)
)
|> Map.values
|> Array.ofSeq
posButton classes [
yield! attrArray
] [str text]
let HtmlPage pageTitle scripts styles tags content =
html [] [
head [] [
title [] [innerText pageTitle]
link [_rel "manifest" ; _href "/manifest.webmanifest"]
yield! styles
yield! scripts
yield! tags
]
body [] [
yield! content
yield! keyboards
]
]
let HTMLPageWithScripts pageTitle scripts content = HtmlPage pageTitle scripts [||] content
let HTMLPageWithStyles pageTitle styles content = HtmlPage pageTitle [||] styles content
let HTMLPageWithNoScriptsOrStyles pageTitle content = HtmlPage pageTitle [||] [||] content

32
Installer/Controller.fs Normal file
View File

@@ -0,0 +1,32 @@
module DredgePos.Installer.Controller
open System.Reflection
open DredgePos.Types
open FSharp.Reflection
let RunMigration (fsModule: System.Type) =
fsModule
.GetMethod("run")
.Invoke(null, [||])
|> ignore
Entity.Create {name=fsModule.FullName; timestamp=DredgeFramework.CurrentTime(); id=0} |> ignore
fsModule.FullName + " ran Successfully"
let RunAllMigrations () =
let completedMigrations =
try
Entity.GetAll<migration>
with
| _ -> [||]
|> Array.map (fun migration -> migration.name)
Assembly
.GetExecutingAssembly()
.GetTypes()
|> Array.filter FSharpType.IsModule
|> Array.filter (fun fsModule -> fsModule.Namespace = "DredgePos.Migrations")
|> Array.filter (fun fsModule -> not (completedMigrations |> Array.contains fsModule.FullName))
|> Array.sortBy (fun fsModule -> fsModule.Name)
|> Array.map RunMigration
|> (fun arr -> if arr.Length > 0 then arr else [|"No Migrations Were Run"|])
|> String.concat "<br/><hr/>"

2
Installer/Model.fs Normal file
View File

@@ -0,0 +1,2 @@
module DredgePos.Installer.Model

13
Installer/Router.fs Normal file
View File

@@ -0,0 +1,13 @@
module DredgePos.Installer.Router
open DredgePos
open Saturn
open Giraffe
let installer = (warbler (fun _ -> htmlString (Controller.RunAllMigrations ())))
let router = router {
pipe_through Ajax.Router.pipeline
get "/" installer
get "" installer
}

View File

@@ -0,0 +1,131 @@
module DredgePos.Migrations.CreateDatabaseSchema
open DredgePos.Types
open Database
open Dapper.FSharp
open Dapper.FSharp.PostgreSQL
let CreateDatabase (db: db_config) connectionString =
let connection = connect connectionString
connection
|> NonDbSpecificQuery $"""
CREATE DATABASE {db.db_name};
"""
let addTables () =
CreateTable "sessions" [
"session_id", "varchar(200)"
"clerk_json", "text"
"clerk_id", "int"
"expires", "int"
]
CreateTable "grids" [
"name", "varchar(60)"
"rows", "int"
"cols", "int"
"data", "text"
]
CreateTable "reservations" [
"name", "varchar(100)"
"time", "int"
"covers", "int"
"floorplan_table_id", "int"
"created_at", "int"
]
CreateTable "venues" [
"name", "varchar(60)"
]
CreateTable "floorplan_tables" [
"table_number", "int"
"room_id", "int"
"venue_id", "int"
"pos_x", "int"
"pos_y", "int"
"shape", "varchar(12)"
"width", "int"
"height", "int"
"default_covers", "int"
"rotation", "int"
"merged_children", "text"
"previous_state", "text"
"status", "int"
]
CreateTable "print_groups" [
"name", "varchar(20)"
"printer_id", "int"
"venue_id", "int"
]
CreateTable "sales_categories" [
"parent", "int"
"name", "varchar(20)"
"print_group_id", "int"
"venue_id", "int"
]
CreateTable "rooms" [
"name", "varchar(20)"
"background_image", "varchar(100)"
"venue_id", "int"
]
CreateTable "floorplan_decorations" [
"room_id", "int"
"pos_x", "int"
"pos_y", "int"
"rotation", "int"
"width", "int"
"height", "int"
"image", "varchar(100)"
"venue_id", "int"
]
CreateTable "clerks" [
"name", "varchar(20)"
"login_code", "int"
"user_group_id", "int"
]
CreateTable "order_screen_page_groups" [
"order", "int"
"venue_id", "int"
"label", "varchar(40)"
"grid_id", "int"
]
CreateTable "buttons" [
"text", "varchar(60)"
"primary_action", "varchar(15)"
"primary_action_value", "varchar(20)"
"secondary_action", "varchar(15)"
"secondary_action_value", "varchar(20)"
"image", "varchar(60)"
"extra_classes", "text"
"extra_styles", "text"
]
CreateTable "items" [
"code", "varchar(40)"
"sales_category_id", "int"
"name", "varchar(60)"
"item_type", "varchar(12)"
"price1", "int"
]
CreateTable "migrations" [
"name", "varchar(100)"
"timestamp", "int"
]
let run () =
let db = getDatabaseSettings ()
$"Server={db.host};Port={db.port};User Id={db.username};Password={db.password};Include Error Detail=true"
|> CreateDatabase db
|> ignore
|> addTables

View File

@@ -0,0 +1,485 @@
module DredgePos.Migrations.PopulateTestData
open DredgeFramework
open DredgePos.Types
open System.IO
let spaceButton () = (Entity.GetFirstByColumn<button> "primary_action" "spacer").id
let getPageOrder pageName =
match pageName with
| "entrees" -> 1
| "mains" -> 2
| "dessert" -> 3
| "beer" -> 4
| "wine" -> 5
| _ -> 0
let CreatePageFromDirectory index (dir: string) =
let dirName = DirectoryInfo(dir).Name
let printGroup =
match dirName.ToLower() with
| "beer" | "wine" -> (Entity.GetFirstByColumn<print_group> "name" "Drinks").id
| "entrees" | "dips" -> (Entity.GetFirstByColumn<print_group> "name" "Entrees").id
| "dessert" -> (Entity.GetFirstByColumn<print_group> "name" "Desserts").id
| _ -> (Entity.GetFirstByColumn<print_group> "name" "Mains").id
let parentName =
match dirName.ToLower() with
| "beer" | "wine" -> "Beverage"
| _ -> "Food"
let parentCategory = Entity.GetFirstByColumn<sales_category> "name" parentName
if dirName.ToLower() <> "dips" && dirName.ToLower() <> "steak temperatures" then
let NewGrid = Entity.Create {
id=0
name=dirName
rows=8
cols=6
data=""
}
Entity.Create {
id=0
order= getPageOrder (dirName.ToLower())
venue_id=1
label=dirName
grid_id=NewGrid.id
} |> ignore
elif dirName.ToLower() = "steak temperatures" then
Entity.Create {
id=0
name=dirName
rows=4
cols=6
data=""
} |> ignore
else ()
Entity.Create {
id=0
parent=parentCategory.id
name=dirName
print_group_id=printGroup
venue_id=1
} |> ignore
dir
let CreateDefaultPrintGroups (path: string) =
Entity.Create {
id=0
name="Entrees"
printer_id=1
venue_id=1
} |> ignore
Entity.Create {
id=0
name="Mains"
printer_id=1
venue_id=1
} |> ignore
Entity.Create {
id=0
name="Desserts"
printer_id=1
venue_id=1
} |> ignore
Entity.Create {
id=0
name="Drinks"
printer_id=2
venue_id=1
} |> ignore
path
let CreateDefaultVenue (path: string) =
{
id=0
name="Megalomania"
}
|> Entity.Create
|>ignore
path
let CreateDefaultClerk (path: string) =
{
id=0
name="Josh"
login_code=1408
user_group_id=1
}
|> Entity.Create
|>ignore
path
let CreateDefaultSalesCategories (path: string) =
Entity.Create {
id=0
parent=0
name="Food"
print_group_id=(Entity.GetFirstByColumn<print_group> "name" "Mains").id
venue_id=1
} |> ignore
Entity.Create {
id=0
parent=0
name="Beverage"
print_group_id=(Entity.GetFirstByColumn<print_group> "name" "Drinks").id
venue_id=1
} |> ignore
path
let CreateDefaultButtons (path: string) =
Entity.Create {
id = 0
text = ""
primary_action = "spacer"
secondary_action = ""
primary_action_value = ""
secondary_action_value = ""
image = ""
extra_classes = "invisible"
extra_styles = ""
}
|> ignore
path
let CreateDefaultItems (path: string) =
Entity.Create {
id = 0
name = "Custom Item"
code = "OPEN000"
sales_category_id = (Entity.GetFirstByColumn<sales_category> "name" "Food").id
item_type = "item"
price1 = 0
}
|> ignore
path
let CreateRooms () =
"wwwroot/images/rooms"
|> Directory.GetFiles
|> Array.filter (fun file -> Path.GetExtension file = ".png" || Path.GetExtension file = ".jpg")
|> Array.iter (fun image ->
let roomName = Path.GetFileNameWithoutExtension image
Entity.Create {
id=0
name=roomName
background_image= Path.GetFileName image
venue_id=1
} |> ignore
)
let populateEntreeGrid () =
let SalesCategory = Entity.GetFirstByColumn<sales_category> "name" "Entrees"
let DipSalesCategory = Entity.GetFirstByColumn<sales_category> "name" "Dips"
let Entrees = Entity.GetAllByColumn<item> "sales_category_id" SalesCategory.id
let Dips = Entity.GetAllByColumn<item> "sales_category_id" DipSalesCategory.id
let space = spaceButton()
let GridData =
[|
space; space; space; space; space; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
|]
|> Array.mapi (fun index current ->
let isFirstColumn = (index % 6) = 0
if not isFirstColumn then current else
let entree = Entrees |> Array.tryItem (index/6)
match entree with
| None -> space
| Some x -> x.id
)
|> Array.mapi (fun index current ->
let isSecondRow = index > 6 && index < 12
if not isSecondRow then current else
let entree = Dips |> Array.tryItem (index-7)
match entree with
| None -> space
| Some x -> x.id
)
let grid =
Entity.GetFirstByColumn<order_screen_page_group> "label" "Entrees"
|> Entity.GetRelated<grid, order_screen_page_group>
let newGrid = {grid with data=(jsonEncode {|page1=GridData|})}
Entity.Update newGrid |> ignore
()
let populateMainGrid (category: string) () =
let SalesCategory = Entity.GetFirstByColumn<sales_category> "name" category
let Mains = Entity.GetAllByColumn<item> "sales_category_id" SalesCategory.id
let space = spaceButton()
let getId index =
match Mains |> Array.tryItem index with
| None -> space
| Some x -> x.id
let GridData =
[|
getId 0; space; getId 1; space; getId 2; space;
space; space; space; space; space; space;
getId 3; space; getId 4; space; getId 5; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
|]
let grid =
Entity.GetFirstByColumn<order_screen_page_group> "label" category
|> Entity.GetRelated<grid, order_screen_page_group>
let newGrid = {grid with data=(jsonEncode {|page1=GridData|})}
Entity.Update newGrid |> ignore
let populateDessertGrid () =
let space = spaceButton()
let SalesCategory = Entity.GetFirstByColumn<sales_category> "name" "Dessert"
let Desserts = Entity.GetAllByColumn<item> "sales_category_id" SalesCategory.id
let getId index =
match Desserts |> Array.tryItem index with
| None -> space
| Some x -> x.id
let GridData =
[|
getId 0; space; getId 1; space; space ; space;
space; space; space; space; space; space;
space; getId 2; space; getId 3; space; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
space; space; space; space; space; space;
|]
let grid =
Entity.GetFirstByColumn<order_screen_page_group> "label" "Dessert"
|> Entity.GetRelated<grid, order_screen_page_group>
let newGrid = {grid with data=(jsonEncode {|page1=GridData|})}
Entity.Update newGrid |> ignore
let populateBeerGrid () =
let space = spaceButton()
let SalesCategory = Entity.GetFirstByColumn<sales_category> "name" "Beer"
let Beers = Entity.GetAllByColumn<item> "sales_category_id" SalesCategory.id
let grid =
Entity.GetFirstByColumn<order_screen_page_group> "label" "Beer"
|> Entity.GetRelated<grid, order_screen_page_group>
let mutable buttonMap = Map.empty<string, int[]>
Beers
|> Array.chunkBySize 24
|> Array.map (fun beerPage ->
let getId index =
match beerPage |> Array.tryItem index with
| None -> space
| Some x -> x.id
[|
getId 0; getId 1; getId 2; getId 3; getId 4 ; getId 5;
space; space; space; space; space; space;
getId 6; getId 7; getId 8; getId 9; getId 10 ; getId 11;
space; space; space; space; space; space;
getId 12; getId 13; getId 14; getId 15; getId 16 ; getId 17;
space; space; space; space; space; space;
getId 18; getId 19; getId 20; getId 21; getId 22 ; getId 23;
space; space; space; space; space; space;
|]
)
|> Array.iteri (fun index buttonIds ->
buttonMap <- buttonMap |> Map.add $"page{index+1}" buttonIds
)
let GridData = buttonMap |> jsonEncode
let newGrid = {grid with data=GridData}
Entity.Update newGrid |> ignore
let populateSteakTemperaturesGrid () =
let space = spaceButton()
let SalesCategory = Entity.GetFirstByColumn<sales_category> "name" "Steak Temperatures"
let Temps = Entity.GetAllByColumn<item> "sales_category_id" SalesCategory.id
let grid = Entity.GetFirstByColumn<grid> "name" "Steak Temperatures"
let getId index =
match Temps |> Array.tryItem index with
| None -> space
| Some x -> x.id
let GridData =
[|
getId 0; space; getId 1; space; getId 2; space;
space; space; space; space; space; space;
getId 3; space; getId 4; space; getId 5; space;
space; space; space; space; space; space;
|]
let newGrid = {grid with data=(jsonEncode {|page1=GridData|}); rows=4; cols=6}
Entity.Update newGrid |> ignore
let steakButtons = Entity.GetAllByColumn<button> "text" "Venison Wellington"
steakButtons |> Array.iter (fun button ->
Entity.Update {button with secondary_action="grid"; secondary_action_value=newGrid.id.ToString()} |> ignore
)
let PopulateGrids () =
populateEntreeGrid ()
|> populateMainGrid "Mains"
|> populateMainGrid "Wine"
|> populateDessertGrid
|> populateBeerGrid
|> populateSteakTemperaturesGrid
let CreateItemFromFileName (index: int) (dirName: string) (file: string) =
let extension = Path.GetExtension file
let fileName = Path.GetFileNameWithoutExtension file
let itemType =
match dirName.ToLower() with
| "dips" | "steak temperatures" -> "instruction"
| _ -> "item"
let categories = (Entity.GetAllByColumn<sales_category> "name" dirName)
let categoryID =
if categories.Length > 0 then categories[0].id
else (Entity.GetFirstByColumn<sales_category> "name" "Mains").id
let newItem = Entity.Create {
id = 0
code = $"{dirName}0{index+1}" |> StringReplace " " ""
sales_category_id=categoryID
name=fileName
item_type=itemType
price1=1000
}
let classes =
match dirName.ToLower() with
| "beer" | "dessert" -> "doubleHeight"
| "mains" | "wine" | "steak temperatures" -> "doubleHeight doubleWidth"
| "entrees" -> "doubleWidth"
| _ -> "normal"
Entity.Create {
id=0
text=fileName
primary_action="item"
primary_action_value=newItem.code
secondary_action="None"
secondary_action_value=""
image= $"{dirName}/{fileName}{extension}"
extra_classes=classes
extra_styles=""
} |> ignore
let CreateItemsAndButtons (dir: string) =
let dirName = DirectoryInfo(dir).Name
dir
|> Directory.GetFiles
|> Array.filter (fun file -> Path.GetExtension file = ".png" || Path.GetExtension file = ".jpg")
|> Array.iteri (fun index -> CreateItemFromFileName index dirName)
let addFloorplanTable tableNumber x y shape width height rotation (room:room) =
Entity.Create {
id=0
table_number=tableNumber
pos_x=x
pos_y=y
shape=shape
width=width
height=height
rotation=rotation
room_id=room.id
venue_id=1
default_covers=2
merged_children=""
previous_state=""
status=0
} |> ignore
room
let PopulateRooms () =
Entity.GetFirstByColumn<room> "name" "Deck & Courtyard"
|> addFloorplanTable 1 1151 1145 "square" 115 115 45
|> addFloorplanTable 2 827 1152 "square" 115 115 0
|> addFloorplanTable 3 836 922 "square" 115 115 0
|> addFloorplanTable 4 956 712 "square" 115 115 0
|> addFloorplanTable 5 535 704 "square" 115 115 0
|> addFloorplanTable 6 265 861 "square" 115 115 0
|> addFloorplanTable 7 265 1031 "square" 115 115 0
|> addFloorplanTable 8 265 1197 "square" 115 115 0
|> addFloorplanTable 19 90 533 "square" 115 115 0
|> addFloorplanTable 20 90 378 "square" 115 115 0
|> addFloorplanTable 21 90 233 "square" 115 115 0
|> addFloorplanTable 22 90 77 "square" 115 115 0
|> addFloorplanTable 23 622 80 "square" 115 115 0
|> addFloorplanTable 24 613 296 "square" 115 115 -45
|> addFloorplanTable 25 498 539 "square" 115 115 0
|> addFloorplanTable 26 854 546 "square" 115 115 0
|> addFloorplanTable 27 932 191 "square" 115 115 45
|> addFloorplanTable 28 1136 79 "square" 115 115 0
|> addFloorplanTable 29 1145 317 "square" 115 115 0
|> addFloorplanTable 30 1145 522 "square" 115 115 0
|> ignore
Entity.GetFirstByColumn<room> "name" "Inside"
|> addFloorplanTable 31 943 1196 "square" 215 90 0
|> addFloorplanTable 32 943 952 "square" 215 90 0
|> addFloorplanTable 33 927 565 "circle" 150 150 0
|> addFloorplanTable 39 725 67 "square" 115 115 0
|> addFloorplanTable 40 685 222 "square" 115 115 0
|> addFloorplanTable 41 685 357 "square" 115 115 0
|> addFloorplanTable 42 725 531 "square" 115 115 0
|> addFloorplanTable 43 185 452 "square" 100 100 0
|> addFloorplanTable 44 185 326 "square" 100 100 0
|> addFloorplanTable 45 185 199 "square" 100 100 0
|> addFloorplanTable 46 185 69 "square" 100 100 0
|> addFloorplanTable 51 685 948 "square" 200 100 0
|> addFloorplanTable 52 685 1198 "square" 200 100 0
|> addFloorplanTable 61 415 762 "circle" 100 100 0
|> addFloorplanTable 62 415 901 "circle" 100 100 0
|> addFloorplanTable 63 415 1039 "circle" 100 100 0
|> addFloorplanTable 64 415 1176 "circle" 100 100 0
|> ignore
Entity.GetFirstByColumn<room> "name" "Function Room"
|> addFloorplanTable 71 1039 361 "square" 110 110 0
|> addFloorplanTable 72 1000 652 "square" 200 100 0
|> addFloorplanTable 73 976 966 "square" 130 130 0
|> addFloorplanTable 74 658 962 "square" 120 115 0
|> addFloorplanTable 75 567 629 "square" 110 200 0
|> addFloorplanTable 76 120 511 "square" 230 100 0
|> addFloorplanTable 77 120 847 "square" 230 100 0
let run () =
"wwwroot/images/items"
|> CreateDefaultVenue
|> CreateDefaultClerk
|> CreateDefaultPrintGroups
|> CreateDefaultSalesCategories
|> CreateDefaultItems
|> CreateDefaultButtons
|> Directory.GetDirectories
|> Array.mapi CreatePageFromDirectory
|> Array.iter CreateItemsAndButtons
|> CreateRooms
|> PopulateGrids
|> PopulateRooms

90
OrderScreen/Controller.fs Normal file
View File

@@ -0,0 +1,90 @@
module DredgePos.OrderScreen.Controller
open DredgePos
open DredgeFramework
open DredgePos.Types
open DredgePos.Global.Controller
open Saturn.CSRF
open Thoth.Json.Net
open Giraffe
open Microsoft.AspNetCore.Http
open FSharp.Collections
let getOrderScreenData (tableNumber: int) =
{|
order_screen_pages = Entity.GetAllInVenue<order_screen_page_group>
sales_categories = Entity.GetAllInVenue<sales_category>
print_groups = Entity.GetAllInVenue<print_group>
custom_item = Entity.GetFirstByColumn<item> "code" "OPEN000"
table = Floorplan.Model.getTable tableNumber
|}
|> ajaxSuccess
|> json
let renderGrid (grid: grid) =
let gridData = grid.data |> Decode.Auto.fromString<Map<string, int[]>>
match gridData with
| Error message -> failwith message
| Ok data ->
data
|> Map.toArray
|> Array.map snd
|> Array.map(
fun buttonIds ->
buttonIds
|> Array.map Entity.GetById<button>
|> Array.map View.itemButton
|> View.gridPage grid
)
let loadGrid (gridId: int) =
let grid = Entity.GetById<grid> gridId
let gridNodes = (renderGrid grid) |> List.ofArray
let gridHtml = Giraffe.ViewEngine.RenderView.AsString.htmlNodes gridNodes
if gridHtml = "Error" then ajaxFail gridHtml
else ajaxSuccess {|grid=grid;gridHtml=gridHtml|}
|> json
let loadOrderScreenView (ctx: HttpContext) (tableNumber: int) =
Authenticate.Model.RequireClerkAuthentication ctx
let tableOption = DredgePos.Floorplan.Model.getTableSafely tableNumber
let attr = Giraffe.ViewEngine.HtmlElements.attr
match tableOption with
| None ->
Browser.redirect "/" ctx
View.posButtonTemplate
| Some table ->
let currentClerk = Authenticate.Model.getCurrentClerk ctx
let styles = [|"dredgepos.orderScreen.css"|] |> addDefaultStyles
let scripts = [|"dredgepos.tables.js";"./external/currency.min.js";"dredgepos.orderScreen.js"; |] |> addDefaultScripts
let metaTags = [|"viewport", "user-scalable = no, initial-scale=0.8,maximum-scale=0.8 ,shrink-to-fit=yes"|] |> addDefaultMetaTags
let printGroupButtons =
Entity.GetAllInVenue<print_group>
|> Array.map View.printGroupButton
let orderScreenPageGroupButtons =
Entity.GetAllInVenue<order_screen_page_group>
|> Array.filter (fun page_group -> page_group.id <> 0)
|> Array.sortBy (fun {order=order} -> order)
|> Array.map View.pageGroupButton
let grids = Model.getAllPageGridsInVenue ()
let pageGroupNodes =
grids
|> Array.map(fun (grid, page_group) ->
renderGrid grid
|> View.pageGroup page_group
)
let coverSelectorButtons =
Array.init (table.default_covers + 1) id
|> Array.map(fun coverNumber ->
let text = if coverNumber > 0 then language.getAndReplace "selected_cover" [coverNumber]
else language.get "cover_zero"
Global.View.PosButton "coverSelectorButton" (map ["data-cover", coverNumber]) text
)
View.index tableNumber styles scripts metaTags currentClerk printGroupButtons orderScreenPageGroupButtons pageGroupNodes coverSelectorButtons

18
OrderScreen/Model.fs Normal file
View File

@@ -0,0 +1,18 @@
module DredgePos.OrderScreen.Model
open DredgeFramework
open DredgePos
open DredgePos.Types
open FSharp.Collections
open Thoth.Json.Net
open Theme
let getAllPageGridsInVenue () =
Entity.GetAllInVenue<order_screen_page_group>
|> Array.filter(fun pageGroup -> pageGroup.grid_id <> 0)
|> Array.map(fun pageGroup -> (Entity.GetById<grid> pageGroup.grid_id), pageGroup)
let printGroupPosButton (printGroup: print_group) =
PosButton (language.getAndReplace "print_with" [printGroup.name]) "printGroupOverrideButton toggle" $"""data-value="{printGroup.id}" """

14
OrderScreen/Router.fs Normal file
View File

@@ -0,0 +1,14 @@
module DredgePos.OrderScreen.Router
open DredgePos
open DredgePos.Types
open Saturn
open Giraffe
let router = router {
pipe_through Ajax.Router.pipeline
getf "/getOrderScreenData/%i" Controller.getOrderScreenData
getf "/getGridHtml/%i" Controller.loadGrid
post "/updateCovers" (bindJson<floorplan_table> (fun table -> Entity.Update table |> Array.head |> DredgeFramework.ajaxSuccess |> json))
getf "/%i" (fun number -> (warbler (fun ctx -> htmlView <| Controller.loadOrderScreenView (snd ctx) number)))
}

167
OrderScreen/View.fs Normal file
View File

@@ -0,0 +1,167 @@
module DredgePos.OrderScreen.View
open DredgeFramework
open DredgePos.Types
open DredgePos.Global.View
open DredgePos.Entities.Buttons.Model
open Thoth.Json.Net
open Giraffe.ViewEngine
open language
let coverSelector buttons = div [_class "coverSelector"] [
yield! buttons
]
let pageContainer floorplanTable (clerk: clerk) printGroupButtons orderScreenPageGroupButtons pageGroups =
div [_id "pageContainer" ; _table floorplanTable] [
div [_id "leftColumn"] [
h1 [_class "tableHeading"] [str (getAndReplace "active_table" [floorplanTable.table_number])]
div [_class "tableInfo"] [
posButton "changeCoverNumberButton" [] [str (getAndReplace "covers" [floorplanTable.default_covers])]
posButton "" [] [str (getAndReplace "logged_in_as" [clerk.name])]
]
div [_class "orderBox"] [
table [_class "orderBoxTable"] [
thead [] [
tr [] [
th [_class "orderBoxCell qtyCell"] [str (get "qty_header")]
th [_class "orderBoxCell itemIdCell"] [str (get "id_header")]
th [_class "orderBoxCell itemCell"] [str (get "item_header")]
th [_class "orderBoxCell unitPriceCell"] [str (get "price_header")]
th [_class "orderBoxCell totalPriceCell"] [str (get "total_price_header")]
th [_class "orderBoxCell printGroupCell"] [str (get "printgroup_header")]
]
]
tbody [] []
]
]
div [_class "orderBoxInfo"] [
span [_class "voidModeWarning"; VisibleInMode ["void"]] [str (get "void_mode")]
]
div [_class "orderBoxFooter"] [
span [_class "orderBoxTotal"] [str (getAndReplace "totalPrice" ["0.00"])]
small [_class "orderBoxSelectedTotal"] [str (getAndReplace "selectedPrice" ["0.00"])]
]
]
div [_id "rightColumn"] [
div [_id "topHalf"] [
div [_class "functionButtons"] [
div [_class "printGroupButtons toggleGroup"] [
input [_type "hidden"; _class "value"; _name "print_override"]
posButton "printGroupOverrideButton toggle default" [
(attr "data-value") (string 0)
] [
["default"] |> getAndReplace "print_with" |> str
]
yield! printGroupButtons
]
div [_class "functionColumn"] [
posButton "accumulateButton" [ActiveInMode "accumulate"] [str (get "accumulate_function")]
posButton "showCoverSelectorButton" [] [str (get "select_covers")]
]
div [_class "functionColumn"] [
posButton "voidButton" [ActiveInMode "void"] [str (get "void")]
posButton "openItemButton" [] [str (get "custom_item_button")]
posButton "freetextButton" [] [str (get "freetext_button")]
posButton "numpadButton" [] [str (get "numpad_button")]
]
div [_class "functionColumn"] [
posButton "" [] ["pay_function" |> get |> str]
posButton "" [] ["print_function" |> get |> str]
]
]
]
div [_id "pageList"] [
yield! orderScreenPageGroupButtons
]
div [_id "pageGroupContainer"] [
yield! pageGroups
]
div [_class "pageNavigation"] [
posButton "prevButton" [] ["prev_page" |> get |> str]
posButton "nextButton" [] ["next_page" |> get |> str]
]
]
]
(* Grid Container, Cover Selector *)
let posButtonTemplate =
template [_id "posButtonTemplate"] [
posButton "" [] []
]
let gridContainer =
div [_class "gridContainer"] [
div [_class "gridContainerHeader"] [
span [] []
div [_class "posButton closeGrid"] [str "×"]
]
div [_class "gridContainerGrid"] [
div [_class "pageGroup"] []
]
div [_class "pageNavigation"] [
posButton "prevButton" [] ["prev_page" |> get |> str]
posButton "nextButton" [] ["next_page" |> get |> str]
]
]
let pageGroupButton (pageGroup: order_screen_page_group) = posButton "loadPageGroup" [(attr "data-page-group-id") (string pageGroup.id)] [str pageGroup.label]
let printGroupButton (printGroup: print_group) = posButton "toggle printGroupOverrideButton" [(attr "data-value") (string printGroup.id)] [ [printGroup.name] |> getAndReplace "print_with" |> str ]
let itemButtonImage (button: button) =
span [
_class "buttonImg"
_style $"background-image:url(\"/images/items/{button.image}\");"
] []
let _data_primary_action = attr "data-primary-action"
let _data_secondary_action = attr "data-secondary-action"
let itemButton (button: button) =
let extraClasses =
if button.image.Length > 0 then button.extra_classes + " hasImage"
else button.extra_classes
let primaryAttributes = getActionAttributes button.primary_action button.primary_action_value
let secondaryAttributes = getActionAttributes button.secondary_action button.secondary_action_value
posButton extraClasses [
yield! primaryAttributes
yield! secondaryAttributes
_style button.extra_styles
_data_primary_action button.primary_action
_data_secondary_action button.secondary_action
] [
if button.image.Length > 0 then itemButtonImage button
span [_class "text "] [str button.text]
]
let _dataPageGroup = attr "data-page-group"
let _dataPageGroupId = attr "data-page-group-id"
let pageGroup (page_group: order_screen_page_group) gridNodes =
div [_class "pageGroup"; _dataPageGroupId (string page_group.id); ] [
yield! gridNodes
]
let gridPage (grid: grid) buttonNodes =
div [
_class "gridPage"
_style $"
grid-template-columns: repeat({grid.cols}, 1fr);
grid-template-rows: repeat({grid.rows}, 1fr);"
] [
yield! buttonNodes
]
let index orderNumber styles scripts tags clerk printGroupButtons orderScreenPageGroupButtons pageGroupNodes coverSelectorButtons =
[|
pageContainer (DredgePos.Floorplan.Model.getTable orderNumber) clerk printGroupButtons orderScreenPageGroupButtons pageGroupNodes
posButtonTemplate
gridContainer
coverSelector coverSelectorButtons
|]
|> HtmlPage "Order" (GetScripts scripts) (GetStyles styles) (GetMetaTags tags)

View File

@@ -1,54 +0,0 @@
module PageController
open Microsoft.AspNetCore.Http
open Floorplan
open Giraffe
open DredgeFramework
let loadHomePage(): HttpHandler =
let variables = map["title", "Log In"]
let scripts = ["dredgepos.authenticate.js"]
let styles = ["dredgepos.authenticate.css"]
htmlString <| Theme.loadTemplateWithVarsScriptsAndStyles "authenticate" variables scripts styles
let loadFloorplan (ctx: HttpContext) : HttpHandler =
Session.RequireClerkAuthentication ctx
let roomMenu =
getRoomList currentVenue
|> Array.map convertRoomListToLinks
|> String.concat "\n"
let variables = map [
"title", "Floorplan"
"roomMenu", roomMenu
"decorator", Decorations.generateDecorator()
]
let styles = ["dredgepos.floorplan.css"]
let scripts = ["external/konva.min.js" ; "dredgepos.floorplan.js"]
let currentClerk = recordToMap <| Session.getCurrentClerk ctx
let arrays = map["clerk", currentClerk]
htmlString <| Theme.loadTemplateWithVarsArraysScriptsAndStyles "floorplan" variables arrays scripts styles
let loadContactPage id =
Session.clerkLogin 1408 |> ignore
Theme.loadTemplate "index"
let getOpenTables() =
let rows = Floorplan.openTables
rows |> jsonEncode
let transferTables() =
Theme.loadTemplate "index"
let mergeTables parent child =
Floorplan.mergeTables parent child |> ignore
"done"
let unmergeTables table =
Floorplan.unmergeTable table |> ignore
"done"

2
Printer.module.fs Normal file
View File

@@ -0,0 +1,2 @@
module Printer

View File

@@ -1,62 +1,26 @@
namespace WebApplication namespace DredgePos
open DredgePos open DredgePos
open Microsoft.AspNetCore.Server.Kestrel.Core
open Reservations
open Saturn open Saturn
open Giraffe open Giraffe
open Types
module Program = module Program =
let router = router {
let handlePostRoute<'a> handlerFunction post next ctx = pipe_through Ajax.Router.pipeline
json (handlerFunction ctx post) next ctx
let browser = pipeline {
use_warbler
}
let ajaxRouter = router {
pipe_through browser
post "/authenticateClerk" (bindJson<int> (handlePostRoute AjaxController.loginWithLoginCode) )
post "/getTableData" (bindJson<int> AjaxController.getTableData)
post "/transformTable" (bindJson<floorplan_table> AjaxController.transformTable)
post "/createTable" (bindJson<floorplan_table> AjaxController.createTable)
post "/addDecoration" (bindJson<floorplan_decoration> AjaxController.AddDecoration)
post "/updateDecoration" (bindJson<floorplan_decoration> AjaxController.UpdateDecoration)
post "/deleteDecoration" (bindJson<floorplan_decoration> AjaxController.DeleteDecoration)
post "/deleteTable" (bindJson<floorplan_table> AjaxController.deleteTable)
post "/mergeTables" (bindJson<floorplan_table[]> AjaxController.mergeTables)
post "/newEmptyReservation" (bindJson<reservation> AjaxController.newEmptyReservation)
post "/updateReservation" (bindJson<reservation> AjaxController.updateReservation)
post "/getReservation" (bindJson<int> (fun reservation -> json <| GetReservationById reservation) )
post "/unreserveTable" (bindJson<floorplan_table> AjaxController.unreserveTable )
getf "/getRoomData/%i" AjaxController.getRoomData
getf "/getKeyboardLayout/%s" AjaxController.getKeyboardLayout
getf "/getTablesAndDecorations/%i" AjaxController.getRoomTablesAndDecorations
get "/languageVars" (json <| AjaxController.getLanguageVars)
get "/getOpenTables" (json <| Floorplan.getActiveTables Floorplan.currentVenue)
getf "/getActiveTables/%i" AjaxController.getActiveTables
getf "/getFloorplanData/%i" AjaxController.getFloorplanData
getf "/tableIsOpen/%i" (fun tableNumber -> json <| Floorplan.tableNumberIsOpen tableNumber)
getf "/transferTable/%i/%i" AjaxController.transferTable
getf "/unmergeTable/%i" AjaxController.unmergeTable
getf "/tableExists/%i" (fun tableNumber -> json <| Floorplan.tableExists tableNumber)
}
let pageRouter = router {
pipe_through browser
not_found_handler (setStatusCode 404 >=> text "404") not_found_handler (setStatusCode 404 >=> text "404")
get "/" (redirectTo true "/login") get "/" (redirectTo true "/login")
get "/login" (warbler (fun _ -> PageController.loadHomePage() )) forward "/ajax" Ajax.Router.router
get "/floorplan" (warbler (fun ctx -> PageController.loadFloorplan (snd ctx))) forward "/floorplan" DredgePos.Floorplan.Router.router
forward "/ajax" ajaxRouter forward "/order" DredgePos.OrderScreen.Router.router
forward "/login" DredgePos.Authenticate.Router.router
forward "/reservations" DredgePos.Reservations.Router.router
forward "/install" DredgePos.Installer.Router.router
} }
let app = application { let app = application {
use_mime_types [(".woff", "application/font-woff")]
use_static "wwwroot" use_static "wwwroot"
use_router pageRouter use_router router
url "http://0.0.0.0:5001" url "http://0.0.0.0:5001"
} }

View File

@@ -0,0 +1,29 @@
module DredgePos.Reservations.Controller
open DredgeFramework
open DredgePos
open DredgePos.Types
open Giraffe
let newEmptyReservation (reservation: reservation) =
let newReservation = {reservation with
created_at = CurrentTime()
time = CurrentTime()
}
if reservation.floorplan_table_id > 0 then
let table = {(Entity.GetById<floorplan_table> reservation.floorplan_table_id) with
status = 2
default_covers = reservation.covers}
Floorplan.Model.updateTablePosition table |> ignore
let createdReservation = Floorplan.Model.createEmptyReservation newReservation
ajaxSuccess createdReservation |> json
let updateReservation (reservation: reservation) = Model.updateReservation reservation |> ajaxSuccess |> json
let unreserveTable (table: floorplan_table) =
let newTable = {table with status = 0}
Floorplan.Model.updateTablePosition newTable |> ignore
Model.DeleteReservation newTable.id
newTable |> ajaxSuccess |> json

View File

@@ -1,29 +1,20 @@
module Reservations module DredgePos.Reservations.Model
open System
open DredgeFramework open DredgeFramework
open Dapper.FSharp open Dapper.FSharp
open DredgePos open DredgePos
open Types open Types
let GetReservationById (id: int) =
select {
table "reservations"
where (eq "id" id)
}
|> db.Select<reservation>
|> first
let updateReservation (reservation: reservation) = let updateReservation (reservation: reservation) =
update{ update{
table "reservations" table "reservations"
set reservation set reservation
where(eq "id" reservation.id) where(eq "id" reservation.id)
} |> db.Update |> ignore } |> Database.Update |> ignore
reservation reservation
let DeleteReservation (tableId: int) = let DeleteReservation (tableId: int) =
delete { delete {
table "reservations" table "reservations"
where (eq "reservation_table_id" tableId) where (eq "floorplan_table_id" tableId)
} |> db.Delete |> ignore } |> Database.Delete |> ignore

13
Reservations/Router.fs Normal file
View File

@@ -0,0 +1,13 @@
module DredgePos.Reservations.Router
open DredgePos
open DredgePos.Types
open Saturn
open Giraffe
let router = router {
pipe_through Ajax.Router.pipeline
post "/newEmptyReservation" (bindJson<reservation> Controller.newEmptyReservation)
post "/updateReservation" (bindJson<reservation> Controller.updateReservation)
post "/unreserveTable" (bindJson<floorplan_table> Controller.unreserveTable )
}

View File

@@ -1,55 +0,0 @@
module DredgePos.Types
[<CLIMutable>]
type reservation = {
id: int
reservation_name: string
reservation_time: int
reservation_covers: int
reservation_table_id: int
reservation_created_at: int
}
[<CLIMutable>]
type floorplan_table = {
table_number: int
room_id: int
venue_id: int
pos_x: int
pos_y: int
shape: string
width: int
height: int
default_covers: int
rotation: int
merged_children: string
previous_state: string
status: int
id: int
}
[<CLIMutable>]
type floorplan_room = {
id: int
room_name: string
background_image: string
venue_id: int
}
[<CLIMutable>]
type floorplan_decoration = {
id: int
decoration_room: int
decoration_pos_x: int
decoration_pos_y: int
decoration_rotation: int
decoration_width: int
decoration_height: int
decoration_image: string
}
[<CLIMutable>]
type clerk = {id: int; clerk_name: string; clerk_login_code: int; clerk_usergroup: int}
[<CLIMutable>]
type session = {id: int; session_id: string; clerk_json: string; clerk_id: int; expires: int}

9
config.json Normal file
View File

@@ -0,0 +1,9 @@
{
"database": {
"db_name": "dredgepos",
"username": "postgres",
"password": "root",
"host": "localhost",
"port": 5432
}
}

907
package-lock.json generated
View File

@@ -1,8 +1,874 @@
{ {
"name": "DredgePos", "name": "DredgePos",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 1, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": {
"": {
"name": "DredgePos",
"version": "1.0.0",
"dependencies": {
"@types/jquery": "^3.5.7",
"canvas": "^2.8.0",
"currency.js": "^2.0.4",
"konva": "^8.2.2",
"sass": "^1.43.4",
"tsc": "^2.0.4",
"typescript": "^4.7.4"
}
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
"integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==",
"dependencies": {
"detect-libc": "^1.0.3",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.1",
"nopt": "^5.0.0",
"npmlog": "^4.1.2",
"rimraf": "^3.0.2",
"semver": "^7.3.4",
"tar": "^6.1.0"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@types/jquery": {
"version": "3.5.7",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.7.tgz",
"integrity": "sha512-Why+9t1KuqWtIqYKtbk6wgWbE1PjyXJOyGkpmTUh0RX5p4HL7nnRuBkjAO9P2r9tGQP6bLWxl77jRLew3V5xXg==",
"dependencies": {
"@types/sizzle": "*"
}
},
"node_modules/@types/sizzle": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ=="
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
},
"node_modules/are-we-there-yet": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
"integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"engines": {
"node": ">=8"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dependencies": {
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/canvas": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-2.8.0.tgz",
"integrity": "sha512-gLTi17X8WY9Cf5GZ2Yns8T5lfBOcGgFehDFb+JQwDqdOoBOcECS9ZWMEAqMSVcMYwXD659J8NyzjRY/2aE+C2Q==",
"hasInstallScript": true,
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.0",
"nan": "^2.14.0",
"simple-get": "^3.0.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/chokidar": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
"integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"engines": {
"node": ">=10"
}
},
"node_modules/code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/currency.js": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/currency.js/-/currency.js-2.0.4.tgz",
"integrity": "sha512-6/OplJYgJ0RUlli74d93HJ/OsKVBi8lB1+Z6eJYS1YZzBuIp4qKKHpJ7ad+GvTlWmLR/hLJOWTykN5Nm8NJ7+w==",
"engines": {
"node": ">=4"
}
},
"node_modules/debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decompress-response": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
"dependencies": {
"mimic-response": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"dependencies": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.0",
"object-assign": "^4.1.0",
"signal-exit": "^3.0.0",
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wide-align": "^1.1.0"
}
},
"node_modules/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
},
"node_modules/https-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dependencies": {
"number-is-nan": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"node_modules/konva": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/konva/-/konva-8.2.2.tgz",
"integrity": "sha512-O6Ybw1V5x/PNX4Tc+h/U9LCuyoVZe+XaWc23XKxKs0xIbFyTkvdAJEsw3XQw+DsMDTO56UiTvspq5pfO75xEOg==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/lavrton"
},
{
"type": "opencollective",
"url": "https://opencollective.com/konva"
},
{
"type": "github",
"url": "https://github.com/sponsors/lavrton"
}
]
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/mimic-response": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minipass": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz",
"integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ=="
},
"node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"dependencies": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
"gauge": "~2.7.3",
"set-blocking": "~2.0.0"
}
},
"node_modules/number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/sass": {
"version": "1.43.4",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.43.4.tgz",
"integrity": "sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=8.9.0"
}
},
"node_modules/semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"node_modules/signal-exit": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz",
"integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ=="
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/simple-get": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz",
"integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==",
"dependencies": {
"decompress-response": "^4.2.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dependencies": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dependencies": {
"ansi-regex": "^2.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tar": {
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
"integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^3.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"node_modules/tsc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/tsc/-/tsc-2.0.4.tgz",
"integrity": "sha512-fzoSieZI5KKJVBYGvwbVZs/J5za84f2lSTLPYf6AGiIf43tZ3GNrI1QzTLcjtyDDP4aLxd46RTZq1nQxe7+k5Q==",
"bin": {
"tsc": "bin/tsc"
}
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
},
"dependencies": { "dependencies": {
"@mapbox/node-pre-gyp": { "@mapbox/node-pre-gyp": {
"version": "1.0.5", "version": "1.0.5",
@@ -386,9 +1252,9 @@
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ=="
}, },
"node-fetch": { "node-fetch": {
"version": "2.6.5", "version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": { "requires": {
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
} }
@@ -517,15 +1383,23 @@
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
}, },
"simple-get": { "simple-get": {
"version": "3.1.0", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz",
"integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==",
"requires": { "requires": {
"decompress-response": "^4.2.0", "decompress-response": "^4.2.0",
"once": "^1.3.1", "once": "^1.3.1",
"simple-concat": "^1.0.0" "simple-concat": "^1.0.0"
} }
}, },
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"string-width": { "string-width": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -536,14 +1410,6 @@
"strip-ansi": "^3.0.0" "strip-ansi": "^3.0.0"
} }
}, },
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"strip-ansi": { "strip-ansi": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
@@ -578,10 +1444,15 @@
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
}, },
"tsc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/tsc/-/tsc-2.0.4.tgz",
"integrity": "sha512-fzoSieZI5KKJVBYGvwbVZs/J5za84f2lSTLPYf6AGiIf43tZ3GNrI1QzTLcjtyDDP4aLxd46RTZq1nQxe7+k5Q=="
},
"typescript": { "typescript": {
"version": "4.4.4", "version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==" "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ=="
}, },
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",

View File

@@ -7,9 +7,15 @@
"currency.js": "^2.0.4", "currency.js": "^2.0.4",
"konva": "^8.2.2", "konva": "^8.2.2",
"sass": "^1.43.4", "sass": "^1.43.4",
"typescript": "^4.4.4" "tsc": "^2.0.4",
"typescript": "^4.7.4"
}, },
"description": "", "description": "",
"license": "", "license": "",
"repository": "" "repository": "",
"scripts": {
"sass": "sass sass:wwwroot/styles",
"typescript": "tsc",
"build": "npm run sass && npm run typescript"
}
} }

View File

@@ -13,4 +13,14 @@
--posbutton-text-color-active: #fff --posbutton-text-color-active: #fff
--posbutton-background: #232B30 -webkit-linear-gradient(top, #3D4850 3%, #000 4%, #333 100%) --posbutton-background: #232B30 -webkit-linear-gradient(top, #3D4850 3%, #000 4%, #333 100%)
--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
/** 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

@@ -1,8 +1,37 @@
@font-face
font-family: "manrope"
src: url("/fonts/OpenSans-Regular.ttf") format('truetype')
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
padding: 0 padding: 0
box-sizing: border-box box-sizing: border-box
font-family: Arial, Helvetica, sans-serif font-family: 'manrope', sans-serif
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
direction: rtl
input[type=text], select, textarea input[type=text], select, textarea
padding-left: 1em padding-left: 1em
@@ -18,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)
@@ -50,17 +80,15 @@ body
background: var(--posbutton-background) background: var(--posbutton-background)
text-shadow: 1px 1px #1f272b text-shadow: 1px 1px #1f272b
border: solid 1px var(--posbutton-border-color) border: solid 1px var(--posbutton-border-color)
overflow: hidden
cursor: pointer
text-decoration: none
.posHeader .posHeader
padding: 0.5em padding: 0.5em
color: var(--pos-header-text-color) color: var(--pos-header-text-color)
background: var(--pos-header-background) background: var(--pos-header-background)
cursor: default
.posButton.active,.posButton:active, .posButton:focus
border-color: var(--posbutton-border-color-active)
color: var(--posbutton-text-color-active)
background: var(--posbutton-background-active) /* webkit */
box-shadow: 1px 1px 1px rgba(255,255,255,0.1) /* CSS3 */
#pageContainer #pageContainer
@include flex @include flex
@@ -69,9 +97,29 @@ body
.posButton, .posHeader .posButton, .posHeader
@include flex @include flex
text-align: center text-align: center
cursor: default
.posButton.active, .posButton:active .posButton.active, .posButton:active
border: inset 2px border: inset 2px
background: var(--posbutton-background-active)
.posButton.voidButton
background: var(--void-button-background)
.invisible
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

@@ -62,6 +62,7 @@
#floorplanCanvas #floorplanCanvas
aspect-ratio: 1/1 aspect-ratio: 1/1
background-repeat: no-repeat background-repeat: no-repeat
background: var(--global-secondary-bgcolor)
> *:not(#floorplanCanvas) > *:not(#floorplanCanvas)
@include flex-column-item @include flex-column-item

View File

@@ -0,0 +1,346 @@
@import dredgepos.keyboards
#leftColumn
@include flex-column
flex-basis: 30%
height: 100%
background-color: grey
> *
@include flex-column-item
.tableHeading
@include flex
color: black
background-color: white
flex-basis: 5%
.tableInfo
@include flex
flex-basis: 5%
> *
@include flex-item
@include flex
.orderBox
flex-basis: 75%
background: var(--global-bgcolor)
overflow-y: auto
.orderBoxInfo
@include flex
flex-basis: 5%
background-color: white
.voidModeWarning
@include flex
@include flex-item
color: red
font-weight: bold
.orderBoxFooter
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
@include flex-column
height: 100%
flex-basis: 70%
background-color: var(--global-bgcolor)
border-left: 2px solid var(--global-border-color)
#topHalf
@include flex-column
@include flex-column-item
flex-basis: 30%
flex-grow: 0
flex-shrink: 0
.functionButtons
@include flex-column-item
@include flex
flex-basis: 100%
> .functionColumn
@include flex-item
@include flex-column
flex-basis: 25%
> *
@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
@include flex
@include flex-column-item
flex-basis: 10%
background-color: var(--global-secondary-bgcolor)
flex-grow: 0
flex-shrink: 0
> *
@include flex-item
border-bottom: solid 2px var(--global-border-color)
.active
border-bottom: none
#pageGroupContainer
@include flex-column
@include flex-column-item
justify-content: flex-end
flex-basis: 45%
height: 50%
scrollbar-width: none
-ms-overflow-style: none
::-webkit-scrollbar
display: none
.pageGroup
/*display: inline-flex*/
@include flex-column-item
flex-basis: 100%
flex-grow: 0
overflow-x: auto
display: none
.gridPage
width: 100%
height: 100%
flex-shrink: 0
flex-grow: 0
display: grid
.doubleWidth
width: calc(200%)
z-index: 10
.doubleHeight
height: calc(200%)
z-index: 10
.hasImage
.buttonImg
background-repeat: no-repeat
background-size: contain
background-position: center
background-origin: content-box
.hasImage.normal
font-size: 0.8em
.buttonImg
flex-basis: 40%
height: 100%
padding: 0.7em
justify-content: flex-end
.text
@include flex
justify-content: flex-start
flex-basis: 60%
height: 100%
.hasImage.doubleHeight
@include flex-column
.buttonImg
padding: 0.6em
flex-basis: 65%
width: 100%
flex-shrink: 0
flex-grow: 0
.text
@include flex
align-items: flex-start
flex-grow: 0
flex-shrink: 0
flex-basis: 35%
width: 100%
overflow: hidden
font-size: 0.9em
.hasImage.doubleWidth
flex-direction: row
.buttonImg
@include flex
flex-basis: 30%
height: 100%
padding-top: 2%
.text
@include flex
flex-basis: 70%
height: 100%
justify-content: flex-start
.hasImage.doubleHeight.doubleWidth
flex-direction: row
.buttonImg
@include flex
flex-basis: 50%
height: 100%
.text
@include flex
flex-basis: 50%
height: 100%
.pageNavigation
@include flex
@include flex-column-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%
.itemIdCell
display: none
.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

10
sql.log Normal file
View File

@@ -0,0 +1,10 @@
create table if not exists migrations
(
id serial
constraint migrations_pk
primary key,
"name" varchar(100) not null,
"timestamp" int not null
);

View File

@@ -7,12 +7,12 @@
"noImplicitAny":true, "noImplicitAny":true,
"removeComments":false, "removeComments":false,
"preserveConstEnums":true, "preserveConstEnums":true,
"outDir":"../js", "outDir":"wwwroot/scripts",
"target":"ES2016", "target":"ES2016",
"sourceMap":true, "sourceMap":true,
"moduleResolution": "node" "moduleResolution": "node"
}, },
"include":[ "include":[
"*" "typescript"
] ]
} }

View File

@@ -1,9 +1,9 @@
let showLoginBox = () => showVirtualNumpad('Enter Login Code', 6, true, false, false, authenticate) let showLoginBox = () => showVirtualNumpad('Enter Login Code', 6, true, false, false, authenticate)
let authenticate = (input : string) => { let authenticate = (input : string) => {
let login = ajaxSync('/ajax/authenticateClerk', input) let login = ajaxSync('/login/authenticateClerk', input)
if(login === 'success'){ if(login === 'success'){
location.assign('/floorplan') location.assign('/floorplan/')
} }
else else
showLoginBox() showLoginBox()

View File

@@ -0,0 +1,241 @@
/// <reference path="./typings/currency.d.ts" />
const Application: ApplicationState = {
keyboard: null,
mode: [],
languageVars: {}
}
/** Parses a language variable. */
const lang = (key: string, replacements?: string[] | string) => {
let finalValue = Application.languageVars[key] || ''
if (!replacements) return finalValue
if (typeof replacements === 'string') replacements = [replacements]
replacements.forEach((replacement, index) => {
const correctIndex = index + 1
finalValue = finalValue.replace(`[${correctIndex}]`, replacement)
})
return finalValue
}
/** Check if a variable is defined */
const defined = (variable: any) => {
return typeof variable !== 'undefined'
}
/** Call an Ajax function asynchronously */
const ajax = (endpoint: string, data: any, method = 'POST', successFunction: Function, errorFunction: Function, beforeFunction: any) => {
data = (data == null) ? data : JSON.stringify(data)
return $.ajax({
url: endpoint,
method: method,
data: data,
success: (response: ajaxResult) => {
if (successFunction && response.status == 'success')
successFunction(JSON.parse(response.data))
else if (errorFunction && response.status != 'success') {
errorFunction(JSON.parse(response.data))
}
},
error: (error) => console.log(error.statusCode),
beforeSend: beforeFunction
})
}
/*
For the flow of the app, synchronous is commonly preferred
though trying to keep its usage as low as possible.
*/
const ajaxSync = (endpoint: string, data?: any, method = 'POST') => {
const response = JSON.parse(
$.ajax({
url: endpoint,
method: method,
data: JSON.stringify(data),
async: false,
}).responseText)
if (response.data) {
response.data = JSON.parse(response.data)
return response.data
}
return response
}
/* Redirect to a specific URL */
const redirect = (url: string): void => location.assign(url)
const resize = () => {
$('#pageContainer').height(window.innerHeight + "px");
}
const setupCore = (languageVars: Record<string, string>) => {
Application.languageVars = languageVars
const doc = $(document)
doc.on('click', '#alertNo, #alertOk', hideAlerts)
doc.on('click', '.toggle', toggle)
window.addEventListener('resize', resize)
resize()
setElementVisibilityByMode()
}
const posAlert = (message: string, title = 'Message') => {
const alertBox = $('#alert')
alertBox.css('display', 'flex');
alertBox.data('value', '');
$('#alertHeading').text(title);
$('#alertMessage').text(message);
$('#alertOk').css('display', 'flex');
$('#alertYes').css('display', 'none');
$('#alertNo').css('display', 'none');
}
const confirmation = (message: string, data: any, title = 'Confirm', submitFunction = (data: any) => {hideAlerts()}) => {
const alert = $('#alert')
$(document).on('click', '#alert #alertYes', () => {
hideAlerts()
submitFunction(data)
$(document).off('click', '#alert #alertYes')
})
alert.css('display', 'flex')
$('#alertHeading').html(title)
$('#alertMessage').html(message)
$('#alertOk').css('display', 'none')
$('#alertYes').css('display', 'flex')
$('#alertNo').css('display', 'flex')
}
const hideAlerts = () => $('#alert').hide()
const turnOnMode = (mode: PosMode) => {
Application.mode.push(mode)
setElementVisibilityByMode()
}
const turnOffMode = (mode: PosMode) => {
Application.mode = Application.mode.filter((value) => value != mode)
setElementVisibilityByMode()
}
const toggleMode = (mode: PosMode) => {
if (!isInMode(mode))
turnOnMode(mode)
else
turnOffMode(mode)
}
const clearModes = () => {
Application.mode = []
setElementVisibilityByMode()
}
const isInMode = (mode: PosMode) => Application.mode.includes(mode)
const setElementVisibilityByMode = () => {
const mode = Application.mode
const elements = $('[data-visible-in-mode]')
elements.each((index, elem) => {
const element = $(elem)
const visibleInModes: PosModes = element.data('visible-in-mode')
const showElement = visibleInModes.every(visibleMode => {
return mode.includes(visibleMode)
});
if (element.hasClass('useVisibility')) {
if (showElement) {
element.css('visibility', 'visible')
} else element.css('visibility', 'hidden')
} else element.toggle(showElement)
})
const invisibleElements = $('[data-invisible-in-mode]')
invisibleElements.each((index, elem) => {
const element = $(elem)
const inVisibleInModes: PosModes = element.data('invisible-in-mode')
const hideElement = inVisibleInModes.some(invisibleMode => {
return mode.includes(invisibleMode)
})
element.toggle(!hideElement)
})
$('[data-active-in-mode]').each((index, elem) => {
const button = $(elem)
const activeInMode: PosMode = button.data('active-in-mode')
mode.includes(activeInMode)
? button.addClass('active')
: button.removeClass('active')
})
}
const getPercentageOfPageContainerWidth = (pixels: number) => ( (pixels / $('#pageContainer').width()) * 100) + '%'
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))

View File

@@ -10,8 +10,8 @@ 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: floorplan_decoration[]
activeTableNumbers: number[] activeTableNumbers: number[]
selectedTableNumber: number selectedTableNumber: number
selectedDecorationId: number selectedDecorationId: number
@@ -24,8 +24,8 @@ interface floorplan{
} }
interface floorplan_data{ interface floorplan_data{
tables: table[] tables: floorplan_table[]
decorations: decoration[] decorations: floorplan_decoration[]
activeTableNumbers: number[] activeTableNumbers: number[]
rooms: room[] rooms: room[]
reservations:reservation[] reservations:reservation[]
@@ -50,7 +50,7 @@ const Floorplan: floorplan = {
selectedDecorationId: 0 selectedDecorationId: 0
}; };
$(() => ajax('/ajax/getFloorplanData/1', null, 'get', setupFloorplan, null, null) ) $(() => ajax('/floorplan/getFloorplanData/1', null, 'get', setupFloorplan, null, null) )
const setupFloorplanEvents = () => { const setupFloorplanEvents = () => {
@@ -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) => {
@@ -113,11 +118,11 @@ const loadRoom = (roomToLoad: room) => {
setupKonva() setupKonva()
$('.roomButton').removeClass('active') $('.roomButton').removeClass('active')
let button = $(`.roomButton[data-value=${roomToLoad.id}]`) let button = $(`.roomButton[data-value=${roomToLoad?.id}]`)
button.addClass('active') button.addClass('active')
const tablesInRoom = Floorplan.tables.filter(table => table.room_id == roomToLoad.id) const tablesInRoom = Floorplan.tables.filter(table => table.room_id == roomToLoad.id)
const decorationsInRoom = Floorplan.decorations.filter(decoration => decoration.decoration_room == roomToLoad.id) const decorationsInRoom = Floorplan.decorations.filter(decoration => decoration.room_id == roomToLoad.id)
decorationsInRoom.forEach(decoration => createDecorationShape(decoration, false)) decorationsInRoom.forEach(decoration => createDecorationShape(decoration, false))
tablesInRoom.forEach(createTableShape) tablesInRoom.forEach(createTableShape)
if(!isInMode('transfer')) { if(!isInMode('transfer')) {
@@ -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({
@@ -168,7 +173,7 @@ const createTableShape = (table: table) => {
stroke: "black", stroke: "black",
strokeWidth: 4, strokeWidth: 4,
draggable: false, draggable: false,
listening: true listening: true,
}); });
break; break;
default: default:
@@ -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
@@ -269,7 +276,7 @@ const saveTable = (tableToUpdate: table) => {
tables.push(tableToUpdate) tables.push(tableToUpdate)
Floorplan.tables = tables Floorplan.tables = tables
ajax("/ajax/transformTable", tableToUpdate, 'post', null,null,null) ajax("/floorplan/transformTable", tableToUpdate, 'post', null,null,null)
} }
const setTransformerNodes = (nodes: Konva.Shape[]) => { const setTransformerNodes = (nodes: Konva.Shape[]) => {
@@ -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,21 +340,21 @@ 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('/reservations/newEmptyReservation', newReservation,'post', emptyReservationCreated, null, null )
} }
const emptyReservationCreated = (reservation: reservation) => { 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,9 +364,9 @@ 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('/reservations/updateReservation', reservation, 'post', reservationNameAdded, null, null)
} }
const reservationNameAdded = (updatedReservation: reservation) => { const reservationNameAdded = (updatedReservation: reservation) => {
@@ -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)
} }
@@ -379,11 +386,11 @@ const updateTableData = (tableToRemove: table) => {
const unreserveTable = () => { const unreserveTable = () => {
const selectedTable = getSelectedTableData() const selectedTable = getSelectedTableData()
selectedTable.status = 0 selectedTable.status = 0
ajax('/ajax/unreserveTable', selectedTable, 'post', tableUnreserved, null, null) ajax('/reservations/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)
} }
@@ -391,7 +398,7 @@ const tableUnreserved = (table: table) => {
const getSelectedTableData = () => getTableDataFromTableNumber(Floorplan.selectedTableNumber) const getSelectedTableData = () => getTableDataFromTableNumber(Floorplan.selectedTableNumber)
const deselectTables = () => { const deselectTables = () => {
Floorplan.stage.find('Rect, Ellipse').forEach( (shape: Konva.Shape, index) => { Floorplan.stage.find('Rect, Ellipse').forEach( (shape: Konva.Shape) => {
shape.stroke('black') shape.stroke('black')
}); });
@@ -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)
@@ -455,21 +469,21 @@ const tableClicked = (event: Konva.KonvaEventObject<any>) => {
} }
const createDecorationShape = (decoration:decoration, select?: boolean) => { const createDecorationShape = (decoration:floorplan_decoration, select?: boolean) => {
const draggable = isInMode('edit') const draggable = isInMode('edit')
const decorationShape = new Image() const decorationShape = new Image()
decorationShape.onload = () => { decorationShape.onload = () => {
const decorationImage = new Konva.Image({ const decorationImage = new Konva.Image({
id: decoration.id.toString(), id: decoration.id.toString(),
x: decoration.decoration_pos_x * Floorplan.visualScale, x: decoration.pos_x * Floorplan.visualScale,
y: decoration.decoration_pos_y * Floorplan.visualScale, y: decoration.pos_y * Floorplan.visualScale,
image: decorationShape, image: decorationShape,
offsetX: decoration.decoration_width * 0.5 * Floorplan.visualScale, offsetX: decoration.width * 0.5 * Floorplan.visualScale,
offsetY: decoration.decoration_height * 0.5 * Floorplan.visualScale, offsetY: decoration.height * 0.5 * Floorplan.visualScale,
rotation: decoration.decoration_rotation, rotation: decoration.rotation,
width: decoration.decoration_width * Floorplan.visualScale, width: decoration.width * Floorplan.visualScale,
height: decoration.decoration_height * Floorplan.visualScale, height: decoration.height * Floorplan.visualScale,
draggable: draggable, draggable: draggable,
}); });
@@ -486,7 +500,7 @@ const createDecorationShape = (decoration:decoration, select?: boolean) => {
} }
} }
decorationShape.src = 'images/decorations/' + decoration.decoration_image decorationShape.src = '/images/decorations/' + decoration.image
} }
const setupDecorationEvents = (decorationShape: Konva.Image) => { const setupDecorationEvents = (decorationShape: Konva.Image) => {
@@ -500,12 +514,14 @@ const decorationClicked = (event: Konva.KonvaEventObject<any>) => {
let decorationShape = event.target as Konva.Image let decorationShape = event.target as Konva.Image
if(isInMode('edit')){ if(isInMode('edit')){
turnOffMode('tableSelected') turnOffMode('tableSelected')
if ((Floorplan.transformer.nodes().length > 0 && Floorplan.transformer.nodes()[0] != decorationShape) || Floorplan.transformer.nodes().length == 0) { if ((isInMode('decorationSelected') && Floorplan.selectedDecorationId != Number(decorationShape.id())) || !isInMode('decorationSelected')) {
selectDecorationShape(decorationShape) selectDecorationShape(decorationShape)
} else { } else {
deselectTables() deselectTables()
decorationShape.moveToBottom() decorationShape.moveToBottom()
} }
} else {
deselectTables()
} }
} }
@@ -525,21 +541,22 @@ const getDecorationDataById = (id: number) => {
const decorationTransformed = (event: Konva.KonvaEventObject<MouseEvent>|Konva.KonvaEventObject<TouchEvent|DragEvent|MouseEvent>) => { const decorationTransformed = (event: Konva.KonvaEventObject<MouseEvent>|Konva.KonvaEventObject<TouchEvent|DragEvent|MouseEvent>) => {
let decorationShape = event.currentTarget as Konva.Image let decorationShape = event.currentTarget as Konva.Image
const oldDecorationData = getDecorationDataById(Number(decorationShape.id())) const oldDecorationData = getDecorationDataById(Number(decorationShape.id()))
const newDecoration: decoration = { const newDecoration: floorplan_decoration = {
id: oldDecorationData.id, id: oldDecorationData.id,
decoration_room: oldDecorationData.decoration_room, room_id: oldDecorationData.room_id,
decoration_pos_x: Math.round(decorationShape.x() / Floorplan.visualScale), pos_x: Math.round(decorationShape.x() / Floorplan.visualScale),
decoration_pos_y: Math.round(decorationShape.y() / Floorplan.visualScale), pos_y: Math.round(decorationShape.y() / Floorplan.visualScale),
decoration_rotation: Math.round(decorationShape.rotation()), rotation: Math.round(decorationShape.rotation()),
decoration_width: Math.round((decorationShape.scaleX() * decorationShape.width()) / Floorplan.visualScale), width: Math.round((decorationShape.scaleX() * decorationShape.width()) / Floorplan.visualScale),
decoration_height: Math.round((decorationShape.scaleY() * decorationShape.height()) / Floorplan.visualScale), height: Math.round((decorationShape.scaleY() * decorationShape.height()) / Floorplan.visualScale),
decoration_image: oldDecorationData.decoration_image, image: oldDecorationData.image,
venue_id: oldDecorationData.venue_id,
} }
saveDecoration(newDecoration) saveDecoration(newDecoration)
} }
const saveDecoration = (decorationToUpdate: decoration) => { const saveDecoration = (decorationToUpdate: floorplan_decoration) => {
const decorations = const decorations =
Floorplan Floorplan
.decorations .decorations
@@ -550,7 +567,7 @@ const saveDecoration = (decorationToUpdate: decoration) => {
decorations.push(decorationToUpdate) decorations.push(decorationToUpdate)
Floorplan.decorations = decorations Floorplan.decorations = decorations
ajax("/ajax/updateDecoration", decorationToUpdate, 'post', null,null,null) ajax("/floorplan/updateDecoration", decorationToUpdate, 'post', null,null,null)
} }
const showDecorator = () => $('#decorator').css('display', 'flex') const showDecorator = () => $('#decorator').css('display', 'flex')
@@ -559,21 +576,22 @@ const hideDecorator = () => $('#decorator').css('display', 'flex').hide()
const addDecoration = (e: Event) => { const addDecoration = (e: Event) => {
const button = $(e.currentTarget) const button = $(e.currentTarget)
const newDecoration: decoration = { const newDecoration: floorplan_decoration = {
id: 0, id: 0,
decoration_room: Floorplan.currentRoom.id, room_id: Floorplan.currentRoom.id,
decoration_pos_x: Floorplan.visualScaleBasis / 2, pos_x: Floorplan.visualScaleBasis / 2,
decoration_pos_y: Floorplan.visualScaleBasis / 2, pos_y: Floorplan.visualScaleBasis / 2,
decoration_rotation: 0, rotation: 0,
decoration_width: 200, width: 200,
decoration_height: 200, height: 200,
decoration_image: button.data('image') image: button.data('image'),
venue_id: Floorplan.currentRoom.venue_id
} }
ajax('/ajax/addDecoration', newDecoration, 'post', decorationAdded, null, null) ajax('/floorplan/addDecoration', newDecoration, 'post', decorationAdded, null, null)
} }
const decorationAdded = (decoration: decoration) => { const decorationAdded = (decoration: floorplan_decoration) => {
Floorplan.decorations.push(decoration) Floorplan.decorations.push(decoration)
createDecorationShape(decoration, true) createDecorationShape(decoration, true)
@@ -582,11 +600,11 @@ const decorationAdded = (decoration: decoration) => {
const deleteDecoration = () => ajax( const deleteDecoration = () => ajax(
'/ajax/deleteDecoration', '/floorplan/deleteDecoration',
getDecorationDataById(Floorplan.selectedDecorationId), getDecorationDataById(Floorplan.selectedDecorationId),
'post', decorationDeleted, null, null) 'post', decorationDeleted, null, null)
const decorationDeleted = (deletedDecoration:decoration) => { const decorationDeleted = (deletedDecoration:floorplan_decoration) => {
Floorplan.decorations = Floorplan.decorations.filter(decoration => decoration.id != deletedDecoration.id) Floorplan.decorations = Floorplan.decorations.filter(decoration => decoration.id != deletedDecoration.id)
const decorationShape = Floorplan.stage.findOne(`#${deletedDecoration.id}`) const decorationShape = Floorplan.stage.findOne(`#${deletedDecoration.id}`)
decorationShape.destroy() decorationShape.destroy()
@@ -597,8 +615,12 @@ const setRoomBackground = (roomToLoad: room) => {
const width = Floorplan.floorplanDiv.width() const width = Floorplan.floorplanDiv.width()
const height = Floorplan.floorplanDiv.height() const height = Floorplan.floorplanDiv.height()
Floorplan.floorplanDiv.css("background-image", `url(images/rooms/${roomToLoad.background_image})`) if(roomToLoad.background_image != "") {
Floorplan.floorplanDiv.css("background-image", `url('/images/rooms/${roomToLoad.background_image}')`)
Floorplan.floorplanDiv.css("background-size", `${width}px ${height}px`) Floorplan.floorplanDiv.css("background-size", `${width}px ${height}px`)
} else {
Floorplan.floorplanDiv.css("background-image", "none")
}
} }
const setupKonva = () => { const setupKonva = () => {
@@ -612,11 +634,14 @@ const setupKonva = () => {
height: dimensions.height, height: dimensions.height,
}) })
Floorplan.stage.on('click', e => { const stageClick = (e: Konva.KonvaEventObject<any> ) => {
if(e.target == Floorplan.stage){ if(e.target == Floorplan.stage){
deselectTables() deselectTables()
} }
}) }
Floorplan.stage.on('click', stageClick)
Floorplan.stage.on('tap', stageClick)
Floorplan.transformer = new Konva.Transformer({ Floorplan.transformer = new Konva.Transformer({
rotationSnaps: [0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 225, 270, -15, -30, -45, -60, -75, -90, -105, -120, -135, -150, -165, -180, -225, -270, 360, -360], rotationSnaps: [0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 225, 270, -15, -30, -45, -60, -75, -90, -105, -120, -135, -150, -165, -180, -225, -270, 360, -360],
@@ -695,7 +720,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,
@@ -712,10 +737,10 @@ const addTable = (tableNumber: number) => {
venue_id: 1 venue_id: 1
}; };
ajax('/ajax/createTable', newTable, 'post', tableAdded, tableNotAdded, null) ajax('/floorplan/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)
@@ -740,10 +765,10 @@ const deleteTable = (tableNumber: number) => {
return false return false
} }
ajax(`/ajax/deleteTable`, tableToDelete, 'post', tableDeleted, null, null); ajax(`/floorplan/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()
@@ -753,16 +778,16 @@ 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'))
return false; return false;
} }
ajax('/ajax/mergeTables', [table1, table2], 'post', tablesMerged, null, null) ajax('/floorplan/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'])
@@ -772,9 +797,9 @@ const tablesMerged = (tables: Record<'child'|'parent'|'merged', table>) => {
tableGroup.draggable(true) tableGroup.draggable(true)
} }
const unmergeTable = () => ajax(`/ajax/unmergeTable/${Floorplan.selectedTableNumber}`, null, 'get', tablesUnmerged, null, null) const unmergeTable = () => ajax(`/floorplan/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']
@@ -786,16 +811,16 @@ 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
} }
ajax(`/ajax/transferTable/${origin.table_number}/${destination.table_number}`, null, 'get', tableTransferred, null, null) ajax(`/floorplan/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

@@ -0,0 +1,617 @@
type OrderScreenData = {
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 = {
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 button = $(e.target)
const container = $('#pageGroupContainer')
$('.loadPageGroup').removeClass('active')
button.addClass('active')
let pageGroupId = button.data('page-group-id')
container.find('.pageGroup').hide()
let activeGrid = $(`.pageGroup[data-page-group-id=${pageGroupId}]`)
let navButtons = container.next('.pageNavigation')
navButtons.css('display', 'flex')
activeGrid.find('.gridPage').length > 1
? navButtons.show()
: navButtons.hide()
activeGrid.css('display', 'inline-flex')
}
const setupOrderScreen = (data: OrderScreenData) => {
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)
doc.on('click', '.nextButton', goToNextPage)
doc.on('click', '.prevButton', goToPrevPage)
doc.on('click', '.loadPageGroup', loadPageGroup)
doc.on('click', getElementsByAction('item'), itemButtonClicked)
doc.on('click', getElementsByAction('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')
let observer = new window.MutationObserver((mutations, observer) => updateOrderBoxTotals())
observer.observe($('.orderBoxTable tbody').get()[0], {
subtree: true,
attributes: true,
childList: true
});
}
const getElementsByAction = (action: string) => `[data-primary-action=${action}], [data-secondary-action=${action}]`
/**
* @param direction 1 for forward, -1 for backwards.
* @param button
*/
const navigatePage = (direction: number, button: JQuery) => {
const grid =
button
.parent()
.parent()
.find('.pageGroup:visible')
grid.get()[0].scrollLeft += grid.width() * direction
}
const goToNextPage = (e: JQuery.TriggeredEvent) => navigatePage(1, $(e.target))
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_id)
.filterByData('cover', orderItem.cover)
.last()
//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.sales_category_id)
const printGroup = OrderScreen.print_group_override ?? OrderScreen.print_groups.where('id', salesCategory.print_group_id)
const orderItem : orderItem = {
id: OrderScreen.order_item_id_generator.next().value,
item: item,
qty: qty,
print_group_id: 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) =>
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
updateOrderBoxTotals()
}
const setOrderItems = (orderItems: orderItem[]) => {
OrderScreen.order_items = orderItems
renderOrderBox()
}
const createOrderRow = (orderItem: orderItem) => {
const row = $('.orderBoxTable').EmptyRow()
const price = money(orderItem.item.price1)
const itemCellText = $('<span/>').text(orderItem.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_id?.name)
.data('order-item-id', orderItem.id)
.data('order-item-id', orderItem.id)
.data('print_group', orderItem.print_group_id)
.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,.unitPriceCell')
.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(`/order/getGridHtml/${grid}`, null, null,gridHtmlGenerated, null, null)
}
const hideGrids = () => $('.gridContainer').hide()
const gridHtmlGenerated = (gridData: {gridHtml:string, grid: grid}) => {
const gridContainer = $('.gridContainer')
const cellDimensions = getGridCellDimensions()
const grid = gridData.grid
const gridHtml = gridData.gridHtml
gridContainer
.show()
.width(cellDimensions.width * grid.cols)
.children('.gridContainerHeader')
.children('span')
.text(grid.name)
.parent()
.parent()
.find('.pageGroup')
.html(gridHtml)
.show()
.parent()
.height(cellDimensions.height * grid.rows)
.closest('.gridContainer')
.find('.pageNavigation')
.toggle(gridContainer.find('.gridPage').length > 1)
.height(cellDimensions.height)
}
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.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.name = text
item.price1 = price.intValue
addNewItem(item)
}
showVirtualNumpad(lang('enter_item_price'), 4, false, true, true, submitFunction)
}
const getGridCellDimensions = () => {
const container = $('#pageGroupContainer')
return {
height: container.height()/8,
width: container.width()/6
}
}
const showCoverSelector = (event: JQuery.TriggeredEvent) => {
const button = $(event.target)
const gridHeight = getGridCellDimensions().height
const coverSelector = $('.coverSelector')
const buttonPositionLeftPercent = getPercentageOfPageContainerWidth(button.offset().left)
const buttonWidthPercent = getPercentageOfPageContainerWidth(button.width())
coverSelector
.toggle(!coverSelector.is(':visible'))
.css({
width: buttonWidthPercent,
left: buttonPositionLeftPercent,
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('/order/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
$('.coverSelector, .gridContainer').hide()
if(OrderScreen.table)
ajax(`/order/getOrderScreenData/${OrderScreen.table.table_number}`, 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

@@ -25,9 +25,9 @@ let showVirtualNumpad = (heading: string, maxlength = 4, isPassword: boolean, al
inputBox.text(''); inputBox.text('');
numpad.data('maxlength', maxlength) numpad.data('maxlength', maxlength)
numpad.data('submitfunction', submitFunction) numpad.data('submit-function', submitFunction)
numpad.data('password', isPassword); numpad.data('password', isPassword);
numpad.data('allowdecimals', allowDecimals); numpad.data('allow-decimals', allowDecimals);
$(document).off('keyup'); $(document).off('keyup');
$(document).on('keyup', e => { $(document).on('keyup', e => {
@@ -64,9 +64,9 @@ let showVirtualNumpad = (heading: string, maxlength = 4, isPassword: boolean, al
let virtualNumpadInput = (input: string) => { let virtualNumpadInput = (input: string) => {
let inputBox = $('#virtualNumpadInput') let inputBox = $('#virtualNumpadInput')
let numpad = $('#virtualNumpad') let numpad = $('#virtualNumpad')
let maxlength = numpad.data('maxlength') let maxlength : number = numpad.data('maxlength')
let allowDecimals = numpad.data('allowdecimals') let allowDecimals: boolean = numpad.data('allow-decimals')
let submitFunction = numpad.data('submitfunction') let submitFunction : Function = numpad.data('submit-function')
let allowedValues = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'submit', 'clear'] let allowedValues = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'submit', 'clear']
let currentValue = numpad.data('value').toString() let currentValue = numpad.data('value').toString()
@@ -144,7 +144,7 @@ let showVirtualNumpad = (heading: string, maxlength = 4, isPassword: boolean, al
inputBox.val('') inputBox.val('')
keyboard.data('maxlength', maxlength) keyboard.data('maxlength', maxlength)
keyboard.data('password', isPassword) keyboard.data('password', isPassword)
keyboard.data('submitfunction', submitFunction) keyboard.data('submit-function', submitFunction)
inputBox.attr('autofocus', 'autofocus'); inputBox.attr('autofocus', 'autofocus');
inputBox.trigger('focus') inputBox.trigger('focus')
inputBox.trigger('click') inputBox.trigger('click')
@@ -175,7 +175,7 @@ let showVirtualNumpad = (heading: string, maxlength = 4, isPassword: boolean, al
break; break;
case 'submit': case 'submit':
hideVirtualKeyboard(); hideVirtualKeyboard();
let submitFunction = keyboard.data('submitfunction') let submitFunction = keyboard.data('submit-function')
submitFunction(inputBox.val()); submitFunction(inputBox.val());
break; break;
case 'shift': case 'shift':
@@ -216,7 +216,6 @@ let showVirtualNumpad = (heading: string, maxlength = 4, isPassword: boolean, al
} }
let setKeyboardLayout = (layout: string, modifier = '') => { let setKeyboardLayout = (layout: string, modifier = '') => {
if (modifier != '') modifier = `_${modifier}` if (modifier != '') modifier = `_${modifier}`
Application.keyboard.currentLayout = layout Application.keyboard.currentLayout = layout
let layoutToLoad = Application.keyboard.layouts[layout] let layoutToLoad = Application.keyboard.layouts[layout]
@@ -226,11 +225,9 @@ let showVirtualNumpad = (heading: string, maxlength = 4, isPassword: boolean, al
We start at 1 instead of 0. Makes it easier for non-programmers We start at 1 instead of 0. Makes it easier for non-programmers
and translators making their own language packs and translators making their own language packs
*/ */
index = index + 1; index = index + 1
// @ts-ignore
let currentRow : string[] = layoutToLoad[`row${index}${modifier}`] let currentRow : string[] = layoutToLoad[`row${index}${modifier}`]
$(row).children('a').each((keyIndex, button) => { $(row).children('a').each((keyIndex, button) => {
let key = $(button); let key = $(button);
let keyValue: string = currentRow[keyIndex]; let keyValue: string = currentRow[keyIndex];

111
typescript/types.ts Normal file
View File

@@ -0,0 +1,111 @@
type PosMode = "edit" | "void" | "transfer" | "default" | "tableSelected" | "decorationSelected" | "activeTableSelected" | "merge" | "reservedTableSelected" | "accumulate"
type PosModes = PosMode[]
interface order {
clerk: string
split: boolean
items: orderItem[]
}
interface orderItem {
id: number
qty: number
print_group_id: print_group
item: item
cover: number
}
interface print_group {
id: number,
name: string,
printer_id: number,
venue_id: number,
}
interface ajaxResult {
status: string
data : any
}
interface ApplicationState {
keyboard: keyboard
mode: PosModes
languageVars: Record<any, string>
}
interface floorplan_table {
table_number: number,
room_id: number
venue_id: number
pos_x: number
pos_y: number
shape: string
width: number
height: number
default_covers: number
rotation: number
merged_children: string
previous_state: string
status: number
id: number
}
interface floorplan_decoration {
id: number
room_id: number
pos_x: number
pos_y: number
rotation: number
width: number
height: number
image: string
venue_id: number
}
interface room {
id: number
name: string
background_image: string
venue_id: number
}
interface reservation {
id: number,
name: string,
time: number,
covers: number,
created_at: number,
floorplan_table_id: number,
}
interface keyboard {
capsLock: boolean
shift: boolean
layouts: VirtualKeyboard
currentLayout: string
}
interface order_screen_page{id: number; order_screen_page_group_id: number; grid_id: number}
interface grid {id: number; name: string; rows: number; cols: number; data: string}
interface item {
id: number
code: string
sales_category_id: number
name: string
item_type: string
price1: number
}
type sales_category = {
id: number
parent: number
name: string
print_group_id: string
venue_id: number
}
interface Array<T> {
where(property: string, value: any): T
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

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