Updates to order screen

This commit is contained in:
2022-01-04 15:04:55 +10:00
parent 4de8f20d00
commit 85722fa692
21 changed files with 720 additions and 242 deletions

View File

@@ -54,9 +54,9 @@ let getFloorplanData (id: int) =
|> json |> json
let getOrderScreenData (id: int) = let getOrderScreenData (id: int) =
let pages = Entity.getAllInVenue<order_screen_page_group>
{| {|
order_screen_pages = pages order_screen_pages = Entity.getAllInVenue<order_screen_page_group>
sales_categories = Entity.getAllInVenue<sales_category>
|} |}
|> ajaxSuccess |> ajaxSuccess
|> json |> json

View File

@@ -15,6 +15,7 @@
<Compile Include="Reservations.module.fs" /> <Compile Include="Reservations.module.fs" />
<Compile Include="Floorplan.module.fs" /> <Compile Include="Floorplan.module.fs" />
<Compile Include="Printer.module.fs" /> <Compile Include="Printer.module.fs" />
<Compile Include="Orders.module.fs" />
<Compile Include="OrderScreen.module.fs" /> <Compile Include="OrderScreen.module.fs" />
<Compile Include="Decorations.module.fs" /> <Compile Include="Decorations.module.fs" />
<Compile Include="Clerk.module.fs" /> <Compile Include="Clerk.module.fs" />

View File

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

View File

@@ -11,7 +11,7 @@ open Theme
let htmlAttributes (attributes: Map<string, string>) = let htmlAttributes (attributes: Map<string, string>) =
" " + (attributes " " + (attributes
|> Map.toArray |> Map.toArray
|> Array.map (fun (attribute, value) -> attribute+"=\""+HttpUtility.HtmlEncode value + "\"") |> Array.map (fun (attribute, value) -> attribute+"='"+HttpUtility.HtmlEncode value + "'")
|> String.concat " ") |> String.concat " ")
let getAllPageGrids () = Entity.getAllInVenue<order_screen_page_group> let getAllPageGrids () = Entity.getAllInVenue<order_screen_page_group>
@@ -46,9 +46,7 @@ let renderButton (buttonId: int) =
then "invisible" then "invisible"
else "" else ""
let image = if button.image.Length > 0 let image = if button.image.Length > 0 then loadTemplateWithVars "orderScreen/button_image" (map ["image", button.image]) else ""
then loadTemplateWithVars "orderScreen/button_image" (map ["image", button.image])
else ""
let extraClasses = [|imageClass; spacerClass|] |> String.concat " " let extraClasses = [|imageClass; spacerClass|] |> String.concat " "
@@ -63,7 +61,7 @@ let renderButton (buttonId: int) =
"extra_styles", extra_styles "extra_styles", extra_styles
"primary_action", button.primary_action "primary_action", button.primary_action
"secondary_action", button.secondary_action "secondary_action", button.secondary_action
"text", if button.text.Length >0 then button.text else action_data.text "text", if button.text.Length > 0 then button.text else action_data.text
"image", image "image", image
"extra_data", action_data.extra_data "extra_data", action_data.extra_data
] ]

3
Orders.module.fs Normal file
View File

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

View File

@@ -36,12 +36,22 @@ let loadFloorplan (ctx: HttpContext) : HttpHandler =
htmlString <| Theme.loadTemplateWithVarsArraysScriptsAndStyles "floorplan" variables arrays scripts styles htmlString <| Theme.loadTemplateWithVarsArraysScriptsAndStyles "floorplan" variables arrays scripts styles
let loadOrderScreen (ctx: HttpContext) : HttpHandler = let loadOrderScreen (ctx: HttpContext) (tableNumber: int) : HttpHandler =
Session.RequireClerkAuthentication ctx Session.RequireClerkAuthentication ctx
let covers = if tableNumber > 0 then (getTable tableNumber).default_covers else 0
let coverString = language.getAndReplace "covers" [covers]
let coverSelectorButton = if tableNumber > 0 then Theme.loadTemplateWithVars "orderScreen/cover_selector_button" (map ["covers", coverString]) else ""
let orderNumber =
if tableNumber > 0 then language.getAndReplace "active_table" [tableNumber]
else language.get "new_order"
let categoryList = let categoryList =
Entity.getAll<order_screen_page_group> Entity.getAllInVenue<order_screen_page_group>
|> Array.filter (fun category -> category.id <> 0) |> Array.filter (fun page_group -> page_group.id <> 0)
|> Array.sortBy (fun {order=order} -> order)
|> Array.map (fun category -> |> Array.map (fun category ->
let categoryMap = recordToMap category let categoryMap = recordToMap category
let categoryArray = map ["page", categoryMap] let categoryArray = map ["page", categoryMap]
@@ -54,15 +64,17 @@ let loadOrderScreen (ctx: HttpContext) : HttpHandler =
|> Array.map OrderScreen.getPagesHTML |> Array.map OrderScreen.getPagesHTML
|> String.concat "\n" |> String.concat "\n"
let variables = map [ let variables = map [
"title", "Order" "title", "Order"
"categoryList", categoryList "categoryList", categoryList
"pageGroups", grids "pageGroups", grids
"orderNumber", orderNumber
"coverSelectorButton", coverSelectorButton
"covers", coverString
] ]
let styles = ["dredgepos.orderScreen.css"] let styles = ["dredgepos.orderScreen.css"]
let scripts = ["dredgepos.orderScreen.js"] let scripts = ["dredgepos.tables.js";"../external/currency.min.js";"dredgepos.orderScreen.js"; ]
let currentClerk = recordToMap <| Session.getCurrentClerk ctx let currentClerk = recordToMap <| Session.getCurrentClerk ctx
let arrays = map ["clerk", currentClerk] let arrays = map ["clerk", currentClerk]

View File

@@ -52,7 +52,8 @@ module Program =
get "/" (redirectTo true "/login") get "/" (redirectTo true "/login")
get "/login" (warbler (fun _ -> PageController.loadHomePage() )) get "/login" (warbler (fun _ -> PageController.loadHomePage() ))
get "/floorplan" (warbler (fun ctx -> PageController.loadFloorplan (snd ctx))) get "/floorplan" (warbler (fun ctx -> PageController.loadFloorplan (snd ctx)))
get "/order" (warbler (fun ctx -> PageController.loadOrderScreen (snd ctx))) get "/order" (warbler (fun ctx -> PageController.loadOrderScreen (snd ctx) 0))
getf "/order/%i" (fun number -> (warbler (fun ctx -> PageController.loadOrderScreen (snd ctx) number)))
forward "/ajax" floorplanRouter forward "/ajax" floorplanRouter
forward "/orderScreen" orderScreenRouter forward "/orderScreen" orderScreenRouter
} }

View File

@@ -29,11 +29,20 @@ type floorplan_table = {
} }
[<CLIMutable>] [<CLIMutable>]
type category = { type print_group = {
id: int id: int
category_name: string name: string
category_print_group: string printer: int
category_department: string venue_id: int
}
[<CLIMutable>]
type sales_category = {
id: int
parent: int
name: string
print_group: string
venue_id: int
} }
[<CLIMutable>] [<CLIMutable>]
@@ -64,7 +73,7 @@ type clerk = {id: int; clerk_name: string; clerk_login_code: int; clerk_usergrou
type session = {id: int; session_id: string; clerk_json: string; clerk_id: int; expires: int} type session = {id: int; session_id: string; clerk_json: string; clerk_id: int; expires: int}
[<CLIMutable>] [<CLIMutable>]
type order_screen_page_group = {id: int; venue_id: int; label: string; grid_id: int} type order_screen_page_group = {id: int; order: int; venue_id: int; label: string; grid_id: int}
[<CLIMutable>] [<CLIMutable>]
type grid = {id: int; grid_name: string; grid_rows: int; grid_cols: int; grid_data: string} type grid = {id: int; grid_name: string; grid_rows: int; grid_cols: int; grid_data: string}
@@ -89,9 +98,9 @@ type item = {
item_category: int item_category: int
item_name: string item_name: string
item_type: string item_type: string
price1: float price1: int
price2: float price2: int
price3: float price3: int
price4: float price4: int
price5: float price5: int
} }

View File

@@ -51,6 +51,8 @@
"merge_table":"Merge Table", "merge_table":"Merge Table",
"unmerge_table":"Unmerge Table", "unmerge_table":"Unmerge Table",
"order_table":"Place Order", "order_table":"Place Order",
"order_number": "Order [1]",
"new_order": "New Order",
"view_table":"View Table", "view_table":"View Table",
"reserve_table":"Reserve Table", "reserve_table":"Reserve Table",
"unreserve_table":"Delete Reservation", "unreserve_table":"Delete Reservation",

View File

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

View File

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

View File

@@ -1,9 +1,25 @@
interface OrderScreen{ type OrderScreenData = {
order_screen_pages: order_screen_page[] order_screen_pages: order_screen_page[]
sales_categories: sales_category[]
}
type OrderScreen = {
order_screen_pages: order_screen_page[]
last_added_item: orderItem
order_items: orderItem[]
sales_categories: sales_category[]
order_item_id_generator: Generator
selected_item_ids: number[]
} }
let OrderScreen : OrderScreen = { let OrderScreen : OrderScreen = {
order_screen_pages: null order_screen_pages: null,
last_added_item: null,
order_items: [],
sales_categories: [],
order_item_id_generator: newestId(),
selected_item_ids: []
} }
const loadPageGroup = (e: Event) => { const loadPageGroup = (e: Event) => {
@@ -14,6 +30,7 @@ const loadPageGroup = (e: Event) => {
$('.pageGroup').hide() $('.pageGroup').hide()
let activeGrid = $(`.pageGroup[data-page-group-id=${pageGroupId}]`) let activeGrid = $(`.pageGroup[data-page-group-id=${pageGroupId}]`)
let navButtons = $('.pageNavigation') let navButtons = $('.pageNavigation')
navButtons.css('display', 'flex')
activeGrid.find('.gridPage').length > 1 activeGrid.find('.gridPage').length > 1
? navButtons.show() ? navButtons.show()
@@ -22,12 +39,20 @@ const loadPageGroup = (e: Event) => {
activeGrid.css('display', 'inline-flex') activeGrid.css('display', 'inline-flex')
} }
const setupOrderScreen = (data: OrderScreen) => { const setupOrderScreen = (data: OrderScreenData) => {
OrderScreen = data OrderScreen.order_screen_pages = data.order_screen_pages
OrderScreen.sales_categories = data.sales_categories
let doc = $(document) let doc = $(document)
doc.on('click', '.nextButton', goToNextPage) doc.on('click', '.nextButton', goToNextPage)
doc.on('click', '.prevButton', goToPrevPage) doc.on('click', '.prevButton', goToPrevPage)
doc.on('click', '.loadPageGroup', loadPageGroup) doc.on('click', '.loadPageGroup', loadPageGroup)
doc.on('click', '[data-primary-action=item]', itemButtonClicked)
doc.on('click', 'tr', itemRowClicked)
doc.on('click', '.voidButton', voidButtonClicked)
doc.on('dblclick', '.voidButton', voidLastItem)
doc.on('click', '.accumulateButton', () => toggleMode('accumulate'))
turnOnMode('accumulate')
$('.loadPageGroup').first().trigger('click') $('.loadPageGroup').first().trigger('click')
} }
@@ -43,5 +68,204 @@ const navigatePage = (direction: number) => {
const goToNextPage = () => navigatePage(1) const goToNextPage = () => navigatePage(1)
const goToPrevPage = () => navigatePage(-1) const goToPrevPage = () => navigatePage(-1)
const addNewItem = (item: item) => {
let salesCategory = OrderScreen.sales_categories.where('id', item.item_category)
const existingOrderItem = isInMode('accumulate') && item.item_type != 'instruction' ? OrderScreen.order_items.where('item', item) : null
let orderItem : orderItem = existingOrderItem || {
id: OrderScreen.order_item_id_generator.next().value,
item: item,
qty: 0,
sales_category: salesCategory,
}
saveOrderItem(orderItem)
renderOrderBox()
}
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)
if(orderItem.id == OrderScreen.last_added_item?.id){
pulseElement(newRow)
}
if(OrderScreen.selected_item_ids.includes(orderItem.id)){
selectRow(newRow)
}
})
tbody.replaceWith(newTbody)
const element = orderBox.find('tbody tr').last().get()[0]
element.scrollIntoView()
OrderScreen.last_added_item = null
}
const createOrderRow = (orderItem: orderItem) => {
const row = $('.orderBoxTable').EmptyRow()
const price = money(orderItem.item.price1)
row.data('order-item-id', orderItem.id)
row.addClass(`${orderItem.item.item_type}Row`)
row
.setColumnValue(lang('qty_header'), orderItem.qty)
.setColumnValue(lang('item_header'), orderItem.item.item_name)
.setColumnValue(lang('price_header'), price)
.setColumnValue(lang('total_price_header'), price)
.setColumnValue(lang('printgroup_header'), OrderScreen.sales_categories.where('id', orderItem.item.item_category)?.name)
.data('order-item-id', orderItem.id)
return row
}
const saveOrderItem = (orderItem: orderItem) => {
const selectedRows = $('.orderBoxTable tbody tr.selected').get()
const currentQty = orderItem.qty
orderItem.qty = currentQty + 1
if( isInMode('accumulate') && orderItem.qty > 1) {
OrderScreen.order_items = OrderScreen.order_items.map(
existingOrderItem => {
if (existingOrderItem == orderItem) return orderItem
else return existingOrderItem
})
} else if(orderItem.item.item_type == 'instruction' && selectedRows.length > 0){
const selectedOrderItemIds : number[] = selectedRows.map(row => {
const orderItem = OrderScreen.order_items.where('id', $(row).data('order-item-id'))
if (orderItem.item && orderItem.item.item_type != 'instruction') {
return orderItem.id
} else {
return null
}
}).filter(number => number)
selectedOrderItemIds.forEach(id => {
let item = OrderScreen.order_items.where('id', id)
let index = OrderScreen.order_items.indexOf(item) + 1
OrderScreen.order_items.splice(index, 0, orderItem)
})
} else {
OrderScreen.order_items.push(orderItem)
}
OrderScreen.last_added_item = orderItem
return orderItem
}
const itemButtonClicked = (e: JQuery.TriggeredEvent) => {
const existingItemRows = $('.itemRow')
const button = $(e.target).closest('.posButton')
const item : item = button.data('item')
if(item.item_type == 'instruction' && existingItemRows.length < 1) return
addNewItem(item)
}
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 id = row.data('order-item-id')
if(!OrderScreen.selected_item_ids.includes(id))
OrderScreen.selected_item_ids.push(id)
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')
OrderScreen.selected_item_ids = OrderScreen.selected_item_ids.filter(id => id != row.data('order-item-id'))
if(row.hasClass('itemRow') && instructionRows.length){
deselectRow(instructionRows)
}
}
const deleteRow = (row: JQuery) => row.find('*:not(.hidden)').slideUp('fast', () => {
OrderScreen.order_items = OrderScreen.order_items.filter(orderItem => orderItem.id != row.data('order-item-id'))
row.remove()
})
const voidInstructionRow = (row: JQuery) => {
const parentRow = row.prevAll('.itemRow').first()
const parentOrderItem = OrderScreen.order_items.where('id', parentRow.data('order-item-id'))
if(!parentRow.hasClass('selected') || (parentOrderItem && parentOrderItem?.qty == 0) || !parentOrderItem)
decrementQty(OrderScreen.order_items.where('id', row.data('order-item-id')))
}
const voidItemRow = (row : JQuery) => {
const newQty = Number(row.getColumnValue(lang('qty_header'))) - 1
const orderItem = OrderScreen.order_items.where('id', row.data('order-item-id'))
const instructionRows = row.nextUntil('.itemRow')
if(newQty < 1)
voidRows(instructionRows)
decrementQty(orderItem)
}
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 = () => {
if(OrderScreen.order_items.length < 1) return
let orderItem = OrderScreen.order_items[OrderScreen.order_items.length-1]
let row = getOrderItemRow(orderItem)
voidRows(row)
}
const decrementQty = (orderItem: orderItem) => {
const row = getOrderItemRow(orderItem)
if(orderItem.qty <= 1){
OrderScreen.order_items = OrderScreen.order_items.filter(item => item != orderItem)
deleteRow(row)
} else {
orderItem.qty--
row.setColumnValue(lang('qty_header'), orderItem.qty)
}
}
const getOrderItemRow = (orderItem: orderItem) => $('tr').filterByData('order-item-id', orderItem.id)
$(() => ajax('/orderScreen/getOrderScreenData/1', null, 'get', setupOrderScreen, null, null) ) $(() => ajax('/orderScreen/getOrderScreenData/1', null, 'get', setupOrderScreen, null, null) )

View File

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

View File

@@ -1,6 +1,25 @@
type PosMode = "edit" | "void" | "transfer" | "default" | "tableSelected" | "decorationSelected" | "activeTableSelected" | "merge" | "reservedTableSelected" type PosMode = "edit" | "void" | "transfer" | "default" | "tableSelected" | "decorationSelected" | "activeTableSelected" | "merge" | "reservedTableSelected" | "accumulate"
type PosModes = PosMode[] type PosModes = PosMode[]
interface order {
clerk: string
split: boolean
items: orderItem[]
}
interface orderItem {
id: number,
qty: number,
sales_category: sales_category
item: item
}
interface printGroup {
id: number,
name: string,
printer: number,
venue_id: number,
}
interface ajaxResult { interface ajaxResult {
status: string status: string
@@ -69,3 +88,27 @@ interface keyboard {
interface order_screen_page{id: number; order_screen_page_group_id: number; grid_id: number} interface order_screen_page{id: number; order_screen_page_group_id: number; grid_id: number}
interface grid {id: number; grid_name: string; grid_rows: number; grid_cols: number; grid_data: string} interface grid {id: number; grid_name: string; grid_rows: number; grid_cols: number; grid_data: string}
interface item {
id: number
item_code: string
item_category: number
item_name: string
item_type: string
price1: number
price2: number
price3: number
price4: number
price5: number
}
type sales_category = {
id: number
parent: number
name: string
print_group: string
venue_id: number
}
interface Array<T> {
where(property: string, value: any): T
}

View File

@@ -1,4 +1,4 @@
declare namespace currency { declare namespace currency {
type Any = number | string | currency; type Any = number | string | currency;
type Format = (currency?: currency, opts?: Options) => string; type Format = (currency?: currency, opts?: Options) => string;
interface Constructor { interface Constructor {
@@ -18,9 +18,9 @@
format?: currency.Format, format?: currency.Format,
fromCents?: boolean fromCents?: boolean
} }
} }
interface currency { interface currency {
add(number: currency.Any): currency; add(number: currency.Any): currency;
subtract(number: currency.Any): currency; subtract(number: currency.Any): currency;
multiply(number: currency.Any): currency; multiply(number: currency.Any): currency;
@@ -33,6 +33,6 @@
toJSON(): number; toJSON(): number;
readonly intValue: number; readonly intValue: number;
readonly value: number; readonly value: number;
} }
declare const currency: currency.Constructor; declare const currency: currency.Constructor;

View File

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

View File

@@ -3,6 +3,15 @@
src: url("/fonts/OpenSans-Regular.ttf") format('truetype') src: url("/fonts/OpenSans-Regular.ttf") format('truetype')
font-style: normal font-style: normal
@font-face
font-family: "manrope"
src: url("/fonts/OpenSans-SemiBold.ttf") format('truetype')
font-weight: bold
@font-face
font-family: "manrope"
src: url("/fonts/OpenSans-Light.ttf") format('truetype')
font-weight: 100
* *
margin: 0 margin: 0
@@ -10,10 +19,19 @@
box-sizing: border-box box-sizing: border-box
font-family: 'manrope', sans-serif font-family: 'manrope', sans-serif
scroll-behavior: smooth scroll-behavior: smooth
cursor: pointer
-webkit-touch-callout: none
-webkit-user-select: none
-khtml-user-select: none
-moz-user-select: none
-ms-user-select: none
user-select: none
.rtl .rtl
direction: rtl direction: rtl
input[type=text], select, textarea input[type=text], select, textarea
padding-left: 1em padding-left: 1em
padding-right: 1em padding-right: 1em
@@ -86,3 +104,19 @@ body
.invisible .invisible
visibility: hidden visibility: hidden
.hidden
display: none
.pulse
animation-name: color
animation-duration: 300ms
animation-iteration-count: 1
@keyframes color
0%
background-color: var(--pulse-first-color)
50%
background-color: var(--pulse-second-color)
100%
background-color: var(--pulse-final-color)

View File

@@ -26,6 +26,7 @@
.orderBox .orderBox
flex-basis: 75% flex-basis: 75%
background: var(--global-bgcolor) background: var(--global-bgcolor)
overflow-y: auto
.orderBoxInfo .orderBoxInfo
flex-basis: 5% flex-basis: 5%
@@ -90,7 +91,7 @@
.active .active
border-bottom: none border-bottom: none
#pageContainer #pageGroupContainer
@include flex-column @include flex-column
@include flex-column-item @include flex-column-item
justify-content: flex-end justify-content: flex-end
@@ -153,13 +154,20 @@
.buttonImg .buttonImg
padding: 0.6em padding: 0.6em
flex-basis: 80% flex-basis: 65%
width: 100% width: 100%
flex-shrink: 0
flex-grow: 0
.text .text
@include flex @include flex
flex-basis: 20% align-items: flex-start
flex-grow: 0
flex-shrink: 0
flex-basis: 35%
width: 100% width: 100%
overflow: hidden
font-size: 0.9em
.hasImage.doubleWidth .hasImage.doubleWidth
flex-direction: row flex-direction: row
@@ -189,11 +197,55 @@
flex-basis: 50% flex-basis: 50%
height: 100% height: 100%
.pageNavigation .pageNavigation
@include flex @include flex
@include flex-column-item @include flex-column-item
display: none display: none
flex-basis: 15%
> * > *
@include flex-item @include flex-item
.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: 70%
td.itemCell
text-align: left
tr.instructionRow
td.itemCell
padding-left: 2em
font-weight: 100
.qtyCell, .totalPriceCell, .printGroupCell
font-size: 0

View File

@@ -2,8 +2,6 @@
<html> <html>
<head> <head>
<title>DredgePOS</title> <title>DredgePOS</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://unpkg.com/current-device/umd/current-device.min.js"></script>
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name = "viewport" content = "user-scalable = no ,shrink-to-fit=yes" /> <meta name = "viewport" content = "user-scalable = no ,shrink-to-fit=yes" />
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -12,13 +10,24 @@
<div id="pageContainer"> <div id="pageContainer">
<div id="leftColumn"> <div id="leftColumn">
<h1 class="tableHeading"><!--[lang:active_table]--></h1> <h1 class="tableHeading"><!--[var: orderNumber]--></h1>
<div class="tableInfo"> <div class="tableInfo">
<a href="#" class="posButton"><!--[lang:covers]--></a> <!--[var: coverSelectorButton]-->
<a class="posHeader">Logged in as <!--[arr:clerk|clerk_name]--></a></a> <a class="posButton">Logged in as <!--[arr:clerk|clerk_name]--></a>
</div> </div>
<div class="orderBox"> <div class="orderBox">
<table class="orderBoxTable">
<thead>
<tr>
<th class="orderBoxCell qtyCell"><!--[lang:qty_header]--></th>
<th class="orderBoxCell itemCell"><!--[lang:item_header]--></th>
<th class="orderBoxCell unitPriceCell hidden"><!--[lang:price_header]--></th>
<th class="orderBoxCell totalPriceCell"><!--[lang:total_price_header]--></th>
<th class="orderBoxCell printGroupCell"><!--[lang:printgroup_header]--></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div> </div>
<div class="orderBoxInfo"></div> <div class="orderBoxInfo"></div>
<div class="orderBoxFooter"> <div class="orderBoxFooter">
@@ -44,8 +53,8 @@
</div> </div>
<div class="functionColumn"> <div class="functionColumn">
<a class="posButton"></a> <a class="posButton"></a>
<a class="posButton"></a> <a class="posButton accumulateButton" data-active-in-mode="accumulate"><!--[lang:accumulate_function]--></a>
<a class="posButton voidButton"><!--[lang:void]--></a> <a class="posButton voidButton" data-active-in-mode="void"><!--[lang:void]--></a>
<a class="posButton"></a> <a class="posButton"></a>
</div> </div>
<div class="functionColumn"></div> <div class="functionColumn"></div>
@@ -55,7 +64,7 @@
<div id="pageList"> <div id="pageList">
<!--[var:categoryList]--> <!--[var:categoryList]-->
</div> </div>
<div id="pageContainer"> <div id="pageGroupContainer">
<!--[var:pageGroups]--> <!--[var:pageGroups]-->
</div> </div>
<div class="pageNavigation"> <div class="pageNavigation">

View File

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

View File

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