diff --git a/AjaxController.fs b/AjaxController.fs index 31a4683..f4a1731 100644 --- a/AjaxController.fs +++ b/AjaxController.fs @@ -54,9 +54,9 @@ let getFloorplanData (id: int) = |> json let getOrderScreenData (id: int) = - let pages = Entity.getAllInVenue {| - order_screen_pages = pages + order_screen_pages = Entity.getAllInVenue + sales_categories = Entity.getAllInVenue |} |> ajaxSuccess |> json diff --git a/DredgePos.fsproj b/DredgePos.fsproj index 5c84b5b..18cab05 100644 --- a/DredgePos.fsproj +++ b/DredgePos.fsproj @@ -15,6 +15,7 @@ + diff --git a/Language.module.fs b/Language.module.fs index 311faa2..8528fbc 100644 --- a/Language.module.fs +++ b/Language.module.fs @@ -23,11 +23,11 @@ let get var = else "Missing language variable: " + var -let getAndReplace languageVar replacements = +let getAndReplace languageVar (replacements: 'x list) = let langString = get languageVar replacements - |> List.mapi (fun index string - -> index + 1, string) + |> List.mapi (fun index replacement + -> index + 1, replacement.ToString()) |> List.fold (fun (result: string) (index, string) -> result.Replace($"[{index}]", string) ) langString \ No newline at end of file diff --git a/OrderScreen.module.fs b/OrderScreen.module.fs index da636c1..3f02775 100644 --- a/OrderScreen.module.fs +++ b/OrderScreen.module.fs @@ -11,7 +11,7 @@ open Theme let htmlAttributes (attributes: Map) = " " + (attributes |> Map.toArray - |> Array.map (fun (attribute, value) -> attribute+"=\""+HttpUtility.HtmlEncode value + "\"") + |> Array.map (fun (attribute, value) -> attribute+"='"+HttpUtility.HtmlEncode value + "'") |> String.concat " ") let getAllPageGrids () = Entity.getAllInVenue @@ -46,9 +46,7 @@ let renderButton (buttonId: int) = then "invisible" else "" - let image = if button.image.Length > 0 - then loadTemplateWithVars "orderScreen/button_image" (map ["image", button.image]) - else "" + let image = if button.image.Length > 0 then loadTemplateWithVars "orderScreen/button_image" (map ["image", button.image]) else "" let extraClasses = [|imageClass; spacerClass|] |> String.concat " " @@ -63,7 +61,7 @@ let renderButton (buttonId: int) = "extra_styles", extra_styles "primary_action", button.primary_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 "extra_data", action_data.extra_data ] diff --git a/Orders.module.fs b/Orders.module.fs new file mode 100644 index 0000000..6a8ebb7 --- /dev/null +++ b/Orders.module.fs @@ -0,0 +1,3 @@ +module Orders + +let getHighestOrderNumber () = 6 \ No newline at end of file diff --git a/PageController.fs b/PageController.fs index dbc1ae6..a839b1b 100644 --- a/PageController.fs +++ b/PageController.fs @@ -36,12 +36,22 @@ let loadFloorplan (ctx: HttpContext) : HttpHandler = htmlString <| Theme.loadTemplateWithVarsArraysScriptsAndStyles "floorplan" variables arrays scripts styles -let loadOrderScreen (ctx: HttpContext) : HttpHandler = +let loadOrderScreen (ctx: HttpContext) (tableNumber: int) : HttpHandler = 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 = - Entity.getAll - |> Array.filter (fun category -> category.id <> 0) + Entity.getAllInVenue + |> Array.filter (fun page_group -> page_group.id <> 0) + |> Array.sortBy (fun {order=order} -> order) |> Array.map (fun category -> let categoryMap = recordToMap category let categoryArray = map ["page", categoryMap] @@ -54,15 +64,17 @@ let loadOrderScreen (ctx: HttpContext) : HttpHandler = |> Array.map OrderScreen.getPagesHTML |> String.concat "\n" - let variables = map [ "title", "Order" "categoryList", categoryList "pageGroups", grids + "orderNumber", orderNumber + "coverSelectorButton", coverSelectorButton + "covers", coverString ] 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 arrays = map ["clerk", currentClerk] diff --git a/Program.fs b/Program.fs index 2d5f28b..e45cafa 100644 --- a/Program.fs +++ b/Program.fs @@ -52,7 +52,8 @@ module Program = get "/" (redirectTo true "/login") get "/login" (warbler (fun _ -> PageController.loadHomePage() )) 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 "/orderScreen" orderScreenRouter } diff --git a/Types.fs b/Types.fs index 3c8445e..daf1637 100644 --- a/Types.fs +++ b/Types.fs @@ -29,11 +29,20 @@ type floorplan_table = { } [] -type category = { +type print_group = { id: int - category_name: string - category_print_group: string - category_department: string + name: string + printer: int + venue_id: int +} + +[] +type sales_category = { + id: int + parent: int + name: string + print_group: string + venue_id: int } [] @@ -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 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} [] 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_name: string item_type: string - price1: float - price2: float - price3: float - price4: float - price5: float -} \ No newline at end of file + price1: int + price2: int + price3: int + price4: int + price5: int +} diff --git a/wwwroot/languages/english/main.json b/wwwroot/languages/english/main.json index a1fa358..45f93c3 100644 --- a/wwwroot/languages/english/main.json +++ b/wwwroot/languages/english/main.json @@ -51,6 +51,8 @@ "merge_table":"Merge Table", "unmerge_table":"Unmerge Table", "order_table":"Place Order", + "order_number": "Order [1]", + "new_order": "New Order", "view_table":"View Table", "reserve_table":"Reserve Table", "unreserve_table":"Delete Reservation", diff --git a/wwwroot/scripts/external/currency.min.js b/wwwroot/scripts/external/currency.min.js new file mode 100644 index 0000000..290d3a3 --- /dev/null +++ b/wwwroot/scripts/external/currency.min.js @@ -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 + +const Application: ApplicationState = { + keyboard: null, + mode: [], + languageVars: {} +} - /** Parses a language variable. */ - let lang = (key: string, replacements?: string[] | string) => { - let finalValue = Application.languageVars[key] || '' +/** 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] + if (!replacements) return finalValue + if (typeof replacements === 'string') replacements = [replacements] - replacements.forEach( (replacement, index) => { - let correctIndex = index+1 - finalValue = finalValue.replace(`[${correctIndex}]`, replacement) - }) + replacements.forEach((replacement, index) => { + const correctIndex = index + 1 + finalValue = finalValue.replace(`[${correctIndex}]`, replacement) + }) - return finalValue - } + return finalValue +} - /** Check if a variable is defined */ - let defined = (variable: any) => { - return typeof variable !== 'undefined' - } +/** Check if a variable is defined */ +const defined = (variable: any) => { + return typeof variable !== 'undefined' +} - /** Call an Ajax function asynchronously */ - let ajax = (endpoint : string, data: any, method = 'POST', successFunction : Function , errorFunction : Function, beforeFunction: any) => { - data = (data == null) ? data : JSON.stringify(data) - return $.ajax({ +/** 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: 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 + 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) => { + Application.languageVars = languageVars + const doc = $(document) + doc.on('click', '#alertNo, #alertOk', hideAlerts) + 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 pulseElement = (element: JQuery) => element.addClass('pulse').on('animationend', () => element.removeClass('pulse')) + +Array.prototype.where = function(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 } +} - /* - For the flow of the app, synchronous is commonly preferred - though trying to keep its usage as low as possible. - */ - let ajaxSync = (endpoint : string, data?: any, method = 'POST') => { - let 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 */ - let redirect = (url: string) : void => location.assign(url) - - - - const resize = () => { - $('#pageContainer').height(window.innerHeight + "px"); - } - - let setupCore = (languageVars: Record) => { - Application.languageVars = languageVars - const doc = $(document) - doc.on('click', '#alertNo, #alertOk', hideAlerts) - window.addEventListener('resize', resize) - resize() - - setElementVisibilityByMode() - } - - - // @ts-ignore - let posAlert = (message: string, title='Message') => { - let 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'); - } - - let confirmation = (message: string, data: any, title='Confirm', submitFunction = (data: any) => {hideAlerts()}) => { - let 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') - } - - - let hideAlerts = () => $('#alert').hide() - - let turnOnMode = (mode : PosMode) => { - Application.mode.push(mode) - setElementVisibilityByMode() - } - - let turnOffMode = (mode : PosMode) => { - Application.mode = Application.mode.filter((value) => value != mode) - setElementVisibilityByMode() - - } - - let toggleMode = (mode: PosMode) => { - if(!isInMode(mode)) - turnOnMode(mode) - else - turnOffMode(mode) - } - - let clearModes = () => {Application.mode = []} - let isInMode = (mode: PosMode) => Application.mode.includes(mode) - - let setElementVisibilityByMode = () => { - const mode = Application.mode - const elements = $('[data-visible-in-mode]') - - elements.each((index, elem) => { - let element = $(elem) - let visibleInModes : PosModes = element.data('visible-in-mode') - - let 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) => { - let element = $(elem) - let inVisibleInModes: PosModes = element.data('invisible-in-mode') - let 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') - - }) - - } - -$( () => ajax('/ajax/languageVars', null, 'GET', setupCore, null, null)) \ No newline at end of file +$(() => ajax('/ajax/languageVars', null, 'GET', setupCore, null, null)) \ No newline at end of file diff --git a/wwwroot/scripts/ts/dredgepos.orderScreen.ts b/wwwroot/scripts/ts/dredgepos.orderScreen.ts index b9f69c6..1a9ad4c 100644 --- a/wwwroot/scripts/ts/dredgepos.orderScreen.ts +++ b/wwwroot/scripts/ts/dredgepos.orderScreen.ts @@ -1,9 +1,25 @@ -interface OrderScreen{ +type OrderScreenData = { 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 = { - 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) => { @@ -14,6 +30,7 @@ const loadPageGroup = (e: Event) => { $('.pageGroup').hide() let activeGrid = $(`.pageGroup[data-page-group-id=${pageGroupId}]`) let navButtons = $('.pageNavigation') + navButtons.css('display', 'flex') activeGrid.find('.gridPage').length > 1 ? navButtons.show() @@ -22,12 +39,20 @@ const loadPageGroup = (e: Event) => { activeGrid.css('display', 'inline-flex') } -const setupOrderScreen = (data: OrderScreen) => { - OrderScreen = data +const setupOrderScreen = (data: OrderScreenData) => { + OrderScreen.order_screen_pages = data.order_screen_pages + OrderScreen.sales_categories = data.sales_categories let doc = $(document) doc.on('click', '.nextButton', goToNextPage) doc.on('click', '.prevButton', goToPrevPage) 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') } @@ -43,5 +68,204 @@ const navigatePage = (direction: number) => { const goToNextPage = () => 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 = $('') + 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) ) \ No newline at end of file diff --git a/wwwroot/scripts/ts/dredgepos.tables.ts b/wwwroot/scripts/ts/dredgepos.tables.ts new file mode 100644 index 0000000..16bbe43 --- /dev/null +++ b/wwwroot/scripts/ts/dredgepos.tables.ts @@ -0,0 +1,47 @@ +interface JQuery { + getColumnValue(columnHeading: string) : string + setColumnValue(columnHeading: string, value: any) : JQuery + getColumnIndex(columnHeading: string) : number + EmptyRow() : JQuery + 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 = $('') + headingCells.each( (index, cell) => { + const newCell = $('') + 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 +} + +$.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 + } + ) +} \ No newline at end of file diff --git a/wwwroot/scripts/ts/types.ts b/wwwroot/scripts/ts/types.ts index 3679a00..aeaf028 100644 --- a/wwwroot/scripts/ts/types.ts +++ b/wwwroot/scripts/ts/types.ts @@ -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[] +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 { status: string @@ -69,3 +88,27 @@ interface keyboard { 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 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 { + where(property: string, value: any): T +} \ No newline at end of file diff --git a/wwwroot/scripts/ts/typings/currency.d.ts b/wwwroot/scripts/ts/typings/currency.d.ts index 54769d5..8626874 100644 --- a/wwwroot/scripts/ts/typings/currency.d.ts +++ b/wwwroot/scripts/ts/typings/currency.d.ts @@ -1,26 +1,26 @@ - declare namespace currency { +declare namespace currency { type Any = number | string | currency; type Format = (currency?: currency, opts?: Options) => string; interface Constructor { - (value: currency.Any, opts?: currency.Options): currency, - new(value: currency.Any, opts?: currency.Options): currency + (value: currency.Any, opts?: currency.Options): currency, + new(value: currency.Any, opts?: currency.Options): currency } interface Options { - symbol?: string, - separator?: string, - decimal?: string, - errorOnInvalid?: boolean, - precision?: number, - increment?: number, - useVedic?: boolean, - pattern?: string, - negativePattern?: string, - format?: currency.Format, - fromCents?: boolean + symbol?: string, + separator?: string, + decimal?: string, + errorOnInvalid?: boolean, + precision?: number, + increment?: number, + useVedic?: boolean, + pattern?: string, + negativePattern?: string, + format?: currency.Format, + fromCents?: boolean } - } +} - interface currency { +interface currency { add(number: currency.Any): currency; subtract(number: currency.Any): currency; multiply(number: currency.Any): currency; @@ -33,6 +33,6 @@ toJSON(): number; readonly intValue: number; readonly value: number; - } +} - declare const currency: currency.Constructor; +declare const currency: currency.Constructor; diff --git a/wwwroot/styles/sass/dark.theme.sass b/wwwroot/styles/sass/dark.theme.sass index 0ae68af..325fbf6 100644 --- a/wwwroot/styles/sass/dark.theme.sass +++ b/wwwroot/styles/sass/dark.theme.sass @@ -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 --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) diff --git a/wwwroot/styles/sass/dredgepos.core.sass b/wwwroot/styles/sass/dredgepos.core.sass index 1cfe8b8..f91f57c 100644 --- a/wwwroot/styles/sass/dredgepos.core.sass +++ b/wwwroot/styles/sass/dredgepos.core.sass @@ -3,6 +3,15 @@ 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 @@ -10,10 +19,19 @@ box-sizing: border-box font-family: 'manrope', sans-serif 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 direction: rtl + + input[type=text], select, textarea padding-left: 1em padding-right: 1em @@ -85,4 +103,20 @@ body background: var(--void-button-background) .invisible - visibility: hidden \ No newline at end of file + 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) \ No newline at end of file diff --git a/wwwroot/styles/sass/dredgepos.orderScreen.sass b/wwwroot/styles/sass/dredgepos.orderScreen.sass index 2777b1f..f3a3c7a 100644 --- a/wwwroot/styles/sass/dredgepos.orderScreen.sass +++ b/wwwroot/styles/sass/dredgepos.orderScreen.sass @@ -26,6 +26,7 @@ .orderBox flex-basis: 75% background: var(--global-bgcolor) + overflow-y: auto .orderBoxInfo flex-basis: 5% @@ -90,7 +91,7 @@ .active border-bottom: none - #pageContainer + #pageGroupContainer @include flex-column @include flex-column-item justify-content: flex-end @@ -100,7 +101,7 @@ -ms-overflow-style: none ::-webkit-scrollbar - display: none + display: none .pageGroup /*display: inline-flex*/ @@ -153,13 +154,20 @@ .buttonImg padding: 0.6em - flex-basis: 80% + flex-basis: 65% width: 100% + flex-shrink: 0 + flex-grow: 0 .text @include flex - flex-basis: 20% + 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 @@ -189,11 +197,55 @@ flex-basis: 50% height: 100% + .pageNavigation @include flex @include flex-column-item display: none - flex-basis: 15% > * - @include flex-item \ No newline at end of file + @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 \ No newline at end of file diff --git a/wwwroot/themes/restaurant/orderScreen.tpl.htm b/wwwroot/themes/restaurant/orderScreen.tpl.htm index 02a9bc4..e4f7c3b 100644 --- a/wwwroot/themes/restaurant/orderScreen.tpl.htm +++ b/wwwroot/themes/restaurant/orderScreen.tpl.htm @@ -1,9 +1,7 @@ - DredgePOS - - + DredgePOS @@ -12,13 +10,24 @@
-

+

- + + + + + + + + + + + +
@@ -44,8 +53,8 @@
- - + +
@@ -55,8 +64,8 @@
-
- +
+