├── public ├── CNAME ├── icons │ ├── favicon.ico │ ├── apple-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── ms-icon-70x70.png │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── apple-icon-precomposed.png │ ├── browserconfig.xml │ └── manifest.json ├── manifest.json └── index.html ├── src ├── setupTests.js ├── components │ ├── Number.re │ ├── ValidationMessage.re │ ├── MenuOption.re │ ├── products │ │ ├── productCard.re │ │ ├── ProductManagement.re │ │ └── ProductEdit.re │ ├── TagCard.re │ ├── ConfigurableDate.re │ ├── logs │ │ ├── LogManagementRow.re │ │ └── LogManagement.re │ ├── DiscountSelector.re │ ├── orderTaking │ │ ├── ProductSearch.re │ │ ├── DisplayOrderItemNotes.re │ │ ├── SkuSearch.re │ │ ├── NotesInput.re │ │ ├── NormalInput.re │ │ ├── OrderItemRow.re │ │ ├── QuantitySelector.re │ │ ├── PinInput.re │ │ ├── OrderItemsNotes.re │ │ ├── PaymentMethodSelector.re │ │ ├── ClosedOrderItems.re │ │ ├── ClosedOrderInfo.re │ │ ├── KeyInput.re │ │ ├── orderActions.re │ │ ├── OrderHelper.re │ │ ├── PayScreen.re │ │ ├── OrderItems.re │ │ └── orderScreen.re │ ├── home │ │ ├── OpenOrderCard.re │ │ ├── home.re │ │ └── openOrders.re │ ├── webhooks │ │ ├── WebhookManagementRow.re │ │ ├── WebhookManagementNew.re │ │ └── WebhookManagement.re │ ├── shared │ │ ├── ItemModal.re │ │ └── ItemManager.re │ ├── Datetime.re │ ├── Button.re │ ├── vendorManagement │ │ ├── VendorManagementNew.re │ │ ├── VendorManagement.re │ │ └── VendorManagementRow.re │ ├── dailyReport │ │ ├── DiscountsReportSection.re │ │ ├── PAndLContainer.re │ │ ├── PAndLReport.re │ │ ├── GrandTotalsSection.re │ │ ├── ExpenseReportSection.re │ │ └── SalesReportSection.re │ ├── DeleteModal.re │ ├── MoneyInput.re │ ├── PercentInput.re │ ├── Admin.re │ ├── discounts │ │ ├── DiscountManagement.re │ │ └── DiscountEdit.re │ ├── Admin.re.orig │ ├── cashiers │ │ ├── CashierManagement.re │ │ └── CashierEdit.re │ ├── DisplayProduct.re │ ├── loading │ │ └── Loading.re │ ├── viewingOrders │ │ └── AllOrders.re │ ├── SearchModal.re │ ├── OrderList.re │ ├── EditableText.re │ ├── SyncManagement.re │ └── AppConfigManagement.re ├── app.re ├── ReactUtils.re ├── __tests__ │ └── components │ │ └── orderTaking │ │ ├── QuantitySelector_test.re │ │ ├── OrderItemNotes_test.re │ │ ├── NotesInput_test.re │ │ ├── DisplayOrderItemNotes_test.re │ │ └── OrderItemRow_test.re ├── appHeader.re ├── index.re ├── app.css ├── ConfigManagement.re ├── loader.css ├── Lang.re ├── logo.svg ├── WebhookEngine.re ├── cafeRouter.re ├── registerServiceWorker.js └── index.css ├── heroku.yml ├── docker-compose.yaml ├── .vscode └── tasks.json ├── Dockerfile ├── .gitignore ├── .gitlab-ci.yml ├── bsconfig.json ├── README.md ├── package.json └── LICENSE.md /public/CNAME: -------------------------------------------------------------------------------- 1 | app.pisto.io -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | require("jest-localstorage-mock"); 2 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: Dockerfile 4 | worker: worker/Dockerfile -------------------------------------------------------------------------------- /public/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/favicon.ico -------------------------------------------------------------------------------- /public/icons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/apple-icon.png -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/favicon-96x96.png -------------------------------------------------------------------------------- /public/icons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/icons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/icons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/icons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/android-icon-36x36.png -------------------------------------------------------------------------------- /public/icons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/android-icon-48x48.png -------------------------------------------------------------------------------- /public/icons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/android-icon-72x72.png -------------------------------------------------------------------------------- /public/icons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/android-icon-96x96.png -------------------------------------------------------------------------------- /public/icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/android-icon-144x144.png -------------------------------------------------------------------------------- /public/icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/android-icon-192x192.png -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | pouchdb: 4 | image: filiosoft/pouchdb 5 | ports: 6 | - "5984:5984" -------------------------------------------------------------------------------- /public/icons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsommardahl/pisto/HEAD/public/icons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /src/components/Number.re: -------------------------------------------------------------------------------- 1 | type t = int; 2 | 3 | let cleanNonNumeric = n => n |> Js.String.replaceByRe([%bs.re "/\\D/g"], ""); 4 | 5 | let fromString = (str: string): t => 6 | switch (str |> cleanNonNumeric) { 7 | | "" => 0 8 | | s => s |> int_of_string 9 | }; 10 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "build", 9 | "problemMatcher": [] 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /public/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | RUN mkdir -p /usr/src/app 4 | WORKDIR /usr/src/app 5 | 6 | COPY package.json /usr/src/app/ 7 | COPY package-lock.json /usr/src/app/ 8 | 9 | RUN npm install 10 | 11 | COPY ./src/ /usr/src/app/src 12 | COPY ./public/ /usr/src/app/public 13 | COPY bsconfig.json /usr/src/app/ 14 | 15 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /src/app.re: -------------------------------------------------------------------------------- 1 | [%bs.raw {|require('./app.css')|}]; 2 | [%bs.raw {|require('moment')|}]; 3 | [%bs.raw {|require('moment/locale/es-do')|}]; 4 | 5 | let component = ReasonReact.statelessComponent("App"); 6 | 7 | let make = _children => { 8 | ...component, 9 | render: _self =>
, 10 | }; -------------------------------------------------------------------------------- /src/components/ValidationMessage.re: -------------------------------------------------------------------------------- 1 | open ReactUtils; 2 | 3 | let component = ReasonReact.statelessComponent("ValidationMessage"); 4 | 5 | let make = (~messageKey="validation.required", ~hidden: bool, _children) => { 6 | ...component, 7 | render: _self => 8 | hidden ? : {sloc(messageKey)} , 9 | }; 10 | -------------------------------------------------------------------------------- /src/ReactUtils.re: -------------------------------------------------------------------------------- 1 | let s = (message: string) => ReasonReact.string(message); 2 | 3 | let sloc = (key: string) => s(key |> Lang.translate); 4 | 5 | let sopt = (str: option(string)) => 6 | switch (str) { 7 | | Some(thing) => ReasonReact.string(thing) 8 | | None => ReasonReact.string("") 9 | }; 10 | 11 | let getVal = ev => ev->ReactEvent.Form.target##value; -------------------------------------------------------------------------------- /src/components/MenuOption.re: -------------------------------------------------------------------------------- 1 | let component = ReasonReact.statelessComponent("MenuOption"); 2 | 3 | let make = (~messageKey: string, ~path: string, _children) => { 4 | ...component, 5 | render: _self => 6 |
ReasonReact.Router.push(path)}> 7 | {ReactUtils.s(messageKey |> Lang.translate)} 8 |
, 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/products/productCard.re: -------------------------------------------------------------------------------- 1 | let component = ReasonReact.statelessComponent("ProductCard"); 2 | 3 | let make = (~product: Product.t, ~onSelect, _children) => { 4 | ...component, 5 | render: _self => 6 | 21 | 22 | , 23 | }; -------------------------------------------------------------------------------- /public/icons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /src/components/shared/ItemModal.re: -------------------------------------------------------------------------------- 1 | let str = ReasonReact.string; 2 | 3 | let component = ReasonReact.statelessComponent("CreateItemModal"); 4 | 5 | let make = 6 | (~onClose=() => (), ~isOpen=false, ~label: string, ~render, _children) => { 7 | ...component, 8 | render: _self => 9 |
10 | 11 | 12 | (ReactUtils.sloc(label)) 13 |
, 26 | }; -------------------------------------------------------------------------------- /src/Lang.re: -------------------------------------------------------------------------------- 1 | type message = { 2 | key: string, 3 | language: string, 4 | content: string, 5 | }; 6 | 7 | let m = (key, language, content) => {key, language, content}; 8 | 9 | [@bs.module] external en : Js.Array.t('a) = "./lang/en.json"; 10 | 11 | [@bs.module] external es : Js.Array.t('a) = "./lang/es.json"; 12 | 13 | let messages = (codex, languageCode) => 14 | codex 15 | |> Js.Array.map(x => m(x##key, languageCode, x##content)) 16 | |> Array.to_list; 17 | 18 | let dictionary: list(message) = 19 | List.concat([messages(en, "EN"), messages(es, "ES")]); 20 | 21 | let translate = key => { 22 | let language = Config.App.get().language; 23 | let matches = 24 | dictionary 25 | |> List.filter((message: message) => 26 | message.key === key && message.language === language 27 | ); 28 | switch (matches |> List.length) { 29 | | 0 => key ++ "." ++ language 30 | | _ => (matches |. List.nth(0)).content 31 | }; 32 | }; -------------------------------------------------------------------------------- /src/components/orderTaking/NotesInput.re: -------------------------------------------------------------------------------- 1 | open ReactUtils; 2 | 3 | type state = {value: string}; 4 | 5 | type action = 6 | | Reset 7 | | Change(string); 8 | 9 | let component = ReasonReact.reducerComponent("NotesInput"); 10 | 11 | let make = (~onFinish, ~className="", _children) => { 12 | ...component, 13 | initialState: () => {value: ""}, 14 | reducer: (action, _state) => 15 | switch (action) { 16 | | Change(text) => ReasonReact.Update({value: text}) 17 | | Reset => ReasonReact.Update({value: ""}) 18 | }, 19 | render: self => 20 |
21 | self.send(Change(getVal(ev)))} 23 | className={"search-input " ++ className} 24 | value={self.state.value} 25 | placeholder="Notes..." 26 | /> 27 |
, 39 | }; -------------------------------------------------------------------------------- /src/components/orderTaking/NormalInput.re: -------------------------------------------------------------------------------- 1 | open ReactUtils; 2 | 3 | type state = {value: string}; 4 | 5 | type action = 6 | | Reset 7 | | Change(string); 8 | 9 | let component = ReasonReact.reducerComponent("NormalInput"); 10 | 11 | let stringOrDefault = (opt: option(string)) => 12 | switch (opt) { 13 | | None => "" 14 | | Some(s) => s 15 | }; 16 | 17 | let make = (~value="", ~onFinish, ~className="", _children) => { 18 | ...component, 19 | initialState: () => {value: value}, 20 | reducer: (action, _state) => 21 | switch (action) { 22 | | Change(text) => ReasonReact.Update({value: text}) 23 | | Reset => ReasonReact.Update({value: ""}) 24 | }, 25 | render: self => 26 |
27 | self.send(Change(getVal(ev)))} 29 | className={"search-input " ++ className} 30 | value={self.state.value} 31 | placeholder="Search a product..." 32 | /> 33 |
, 39 | }; -------------------------------------------------------------------------------- /src/components/Datetime.re: -------------------------------------------------------------------------------- 1 | [%bs.raw {|require('react-datetime/css/react-datetime.css')|}]; 2 | [@bs.module] external reactClass: ReasonReact.reactClass = "react-datetime"; 3 | 4 | [@bs.obj] 5 | external makeProps: 6 | ( 7 | ~className: string=?, 8 | ~locale: string=?, 9 | ~timeFormat: bool=?, 10 | ~input: bool=?, 11 | ~value: Js.Date.t=?, 12 | ~onBlur: MomentRe.Moment.t => unit=?, 13 | ~onFocus: ReactEvent.Focus.t => unit=?, 14 | ~onChange: MomentRe.Moment.t => unit=?, 15 | unit 16 | ) => 17 | _ = 18 | ""; 19 | 20 | let make = 21 | ( 22 | ~className=?, 23 | ~locale=?, 24 | ~value=?, 25 | ~timeFormat=?, 26 | ~input=?, 27 | ~onBlur=?, 28 | ~onFocus=?, 29 | ~onChange=?, 30 | children, 31 | ) => 32 | ReasonReact.wrapJsForReason( 33 | ~reactClass, 34 | ~props= 35 | makeProps( 36 | ~className?, 37 | ~locale?, 38 | ~value?, 39 | ~timeFormat?, 40 | ~input?, 41 | ~onBlur?, 42 | ~onFocus?, 43 | ~onChange?, 44 | (), 45 | ), 46 | children, 47 | ); 48 | -------------------------------------------------------------------------------- /src/components/Button.re: -------------------------------------------------------------------------------- 1 | let component = ReasonReact.statelessComponent("Button"); 2 | 3 | let make = 4 | ( 5 | ~iconClass=?, 6 | ~label=?, 7 | ~subLabel=?, 8 | ~onClick=_ => (), 9 | ~type_="button", 10 | ~disabled=false, 11 | ~local=false, 12 | ~className="", 13 | _children, 14 | ) => { 15 | ...component, 16 | render: _self => 17 | , 43 | }; 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "PistoPOS", 3 | "name": "PistoPOS", 4 | "icons": [ 5 | { 6 | "src": "\/android-icon-36x36.png", 7 | "sizes": "36x36", 8 | "type": "image\/png", 9 | "density": "0.75" 10 | }, 11 | { 12 | "src": "\/android-icon-48x48.png", 13 | "sizes": "48x48", 14 | "type": "image\/png", 15 | "density": "1.0" 16 | }, 17 | { 18 | "src": "\/android-icon-72x72.png", 19 | "sizes": "72x72", 20 | "type": "image\/png", 21 | "density": "1.5" 22 | }, 23 | { 24 | "src": "\/android-icon-96x96.png", 25 | "sizes": "96x96", 26 | "type": "image\/png", 27 | "density": "2.0" 28 | }, 29 | { 30 | "src": "\/android-icon-144x144.png", 31 | "sizes": "144x144", 32 | "type": "image\/png", 33 | "density": "3.0" 34 | }, 35 | { 36 | "src": "\/android-icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image\/png", 39 | "density": "4.0" 40 | } 41 | ], 42 | "start_url": "./index.html", 43 | "display": "standalone", 44 | "theme_color": "#000000", 45 | "background_color": "#ffffff" 46 | } -------------------------------------------------------------------------------- /src/components/home/home.re: -------------------------------------------------------------------------------- 1 | open ReactUtils; 2 | 3 | type state = {customerName: string}; 4 | 5 | type action = 6 | | UpdateCustomerName(string); 7 | 8 | let component = ReasonReact.reducerComponent("Home"); 9 | 10 | let make = (~onStartNewOrder, _children) => { 11 | ...component, 12 | initialState: () => {customerName: ""}, 13 | reducer: (action, _state) => 14 | switch (action) { 15 | | UpdateCustomerName(customerName) => 16 | ReasonReact.Update({customerName: customerName}) 17 | }, 18 | render: self => { 19 | let handleChange = ({ReasonReact.send}, event) => 20 | send(UpdateCustomerName(getVal(event))); 21 |
22 |
23 | 30 |
36 | 37 |
; 38 | }, 39 | }; -------------------------------------------------------------------------------- /src/components/vendorManagement/VendorManagementNew.re: -------------------------------------------------------------------------------- 1 | open ReactUtils; 2 | 3 | type state = { 4 | name: string, 5 | percent: string, 6 | }; 7 | 8 | type action = 9 | | ChangeName(string) 10 | | ClearInputs; 11 | 12 | let component = ReasonReact.reducerComponent("VendorManagementNew"); 13 | 14 | let make = (~create, _children) => { 15 | ...component, 16 | initialState: () => {name: "", percent: ""}, 17 | reducer: (action, state) => 18 | switch (action) { 19 | | ChangeName(newVal) => ReasonReact.Update({...state, name: newVal}) 20 | | ClearInputs => ReasonReact.Update({name: "", percent: ""}) 21 | }, 22 | render: self => { 23 | let finishedEnteringData = () => { 24 | let newVendor: Vendor.New.t = {name: self.state.name}; 25 | self.send(ClearInputs); 26 | create(newVendor); 27 | }; 28 | 29 | 30 | 31 | self.send(ChangeName(getVal(ev)))} 34 | /> 35 | 36 | 37 | 82 | ) 83 | |> Array.of_list 84 | |> ReasonReact.array 85 | ) 86 | ; 87 | }, 88 | }; -------------------------------------------------------------------------------- /src/components/dailyReport/ExpenseReportSection.re: -------------------------------------------------------------------------------- 1 | open ReactUtils; 2 | 3 | let header = 4 | 5 | (sloc("expense.date")) 6 | (sloc("expense.vendor")) 7 | (sloc("expense.description")) 8 | (sloc("expense.subTotals")) 9 | (sloc("expense.tax")) 10 | (sloc("expense.total")) 11 | ; 12 | 13 | let expenseRow = (e: Expense.denormalized) => 14 | 15 | (s(e.date |> Date.toDisplayDate)) 16 | (s(e.vendor.name)) 17 | (s(e.description)) 18 | (s(e.subTotal |> Money.toDisplay)) 19 | (s(e.tax |> Money.toDisplay)) 20 | (s(e.total |> Money.toDisplay)) 21 | ; 22 | 23 | let expensesBody = (title: string, expenses: list(Expense.denormalized)) => { 24 | let grandSubTotal = 25 | expenses |. Belt.List.reduce(0, (a, c) => a + c.subTotal); 26 | let grandTax = expenses |. Belt.List.reduce(0, (a, c) => a + c.tax); 27 | let grandTotal = expenses |. Belt.List.reduce(0, (a, c) => a + c.total); 28 | 29 | 30 |

(s(title))

31 | 32 | header 33 | ( 34 | expenses 35 | |> List.map((e: Expense.denormalized) => expenseRow(e)) 36 | |> Array.of_list 37 | |> ReasonReact.array 38 | ) 39 | 40 | 41 | 42 | 43 | (s(grandSubTotal |> Money.toDisplay)) 44 | (s(grandTax |> Money.toDisplay)) 45 | (s(grandTotal |> Money.toDisplay)) 46 | 47 | ; 48 | }; 49 | 50 | let component = ReasonReact.statelessComponent("ExpenseReportSection"); 51 | 52 | let make = (~expenses: list(Expense.t), ~key="", _children) => { 53 | ...component, 54 | render: _self => { 55 | let denormalized = expenses |> Expense.denormalize; 56 | let groups = 57 | denormalized 58 | |> Group.by((x: Expense.denormalized) => { 59 | let pre = "daily.expensesSection.title.pre" |> Lang.translate; 60 | let post = "daily.expensesSection.title.post" |> Lang.translate; 61 | let title = 62 | x.expenseType.name 63 | ++ " " 64 | ++ pre 65 | ++ " " 66 | ++ (x.taxRate |> Percent.toDisplay) 67 | ++ " " 68 | ++ post; 69 | title; 70 | }); 71 |
72 | 73 | ( 74 | groups 75 | |> List.map((g: Group.group(Expense.denormalized)) => 76 | expensesBody(g.key, g.value) 77 | ) 78 | |> Array.of_list 79 | |> ReasonReact.array 80 | ) 81 |
82 |
; 83 | }, 84 | }; -------------------------------------------------------------------------------- /src/components/dailyReport/SalesReportSection.re: -------------------------------------------------------------------------------- 1 | open ReactUtils; 2 | 3 | let component = ReasonReact.statelessComponent("SalesReportSection"); 4 | 5 | let make = (~title: string, ~sales: list(Sale.t), ~key="", _children) => { 6 | ...component, 7 | render: _self => { 8 | let groups: list(ProductGroup.t) = sales |> ProductGroup.fromSalesList; 9 | let grandSubTotal = 10 | groups |. Belt.List.reduce(0, (a, c) => a + c.subTotal); 11 | let grandTax = groups |. Belt.List.reduce(0, (a, c) => a + c.tax); 12 | let grandTotal = groups |. Belt.List.reduce(0, (a, c) => a + c.total); 13 |
14 |

(s(title))

15 | 16 | 17 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 37 | 38 | 39 | ( 40 | groups 41 | |> List.map((group: ProductGroup.t) => 42 | 43 | 44 | 47 | 50 | 53 | 56 | 59 | 60 | ) 61 | |> Array.of_list 62 | |> ReasonReact.array 63 | ) 64 | 65 | 66 | 67 | 73 | 76 | 79 | 80 | 81 |
19 | (sloc("daily.salesSection.product")) 20 | 22 | (sloc("daily.salesSection.price")) 23 | 25 | (sloc("daily.salesSection.quantity")) 26 | 28 | (sloc("daily.salesSection.subTotal")) 29 | 31 | (sloc("daily.salesSection.tax")) 32 | 34 | (sloc("daily.salesSection.total")) 35 |
(s(group.productName)) 45 | (s(group.salePrice |> Money.toDisplay)) 46 | 48 | (s(group.quantity |> string_of_int)) 49 | 51 | (s(group.subTotal |> Money.toDisplay)) 52 | 54 | (s(group.tax |> Money.toDisplay)) 55 | 57 | (s(group.total |> Money.toDisplay)) 58 |
68 | 69 | 70 | 71 | (s(grandSubTotal |> Money.toDisplay)) 72 | 74 | (s(grandTax |> Money.toDisplay)) 75 | 77 | (s(grandTotal |> Money.toDisplay)) 78 |
82 |
; 83 | }, 84 | }; -------------------------------------------------------------------------------- /src/components/webhooks/WebhookManagementNew.re: -------------------------------------------------------------------------------- 1 | open ReactUtils; 2 | 3 | type state = { 4 | name: string, 5 | url: string, 6 | event: string, 7 | source: string, 8 | behavior: string, 9 | payload: string, 10 | }; 11 | 12 | type action = 13 | | ChangeName(string) 14 | | ChangeUrl(string) 15 | | ChangeEvent(string) 16 | | ChangeSource(string) 17 | | ChangeBehavior(string) 18 | | ChangePayload(string) 19 | | ClearInputs; 20 | 21 | let component = ReasonReact.reducerComponent("WebhookManagementNew"); 22 | 23 | let make = (~create, _children) => { 24 | ...component, 25 | initialState: () => { 26 | name: "", 27 | url: "", 28 | event: "", 29 | source: "Order", 30 | behavior: "FireAndForget", 31 | payload: Webhook.PayloadType.(Json |> toString), 32 | }, 33 | reducer: (action, state) => 34 | switch (action) { 35 | | ChangeName(newVal) => ReasonReact.Update({...state, name: newVal}) 36 | | ChangeUrl(newVal) => ReasonReact.Update({...state, url: newVal}) 37 | | ChangeEvent(newVal) => ReasonReact.Update({...state, event: newVal}) 38 | | ChangeSource(newVal) => ReasonReact.Update({...state, source: newVal}) 39 | | ChangeBehavior(newVal) => 40 | ReasonReact.Update({...state, behavior: newVal}) 41 | | ChangePayload(newVal) => 42 | ReasonReact.Update({...state, payload: newVal}) 43 | | ClearInputs => ReasonReact.Update({...state, name: "", url: ""}) 44 | }, 45 | render: self => { 46 | let finishedEnteringData = () => { 47 | let newWebhook: Webhook.New.t = { 48 | name: self.state.name, 49 | url: self.state.url, 50 | event: self.state.event |> Webhook.EventType.toT, 51 | source: self.state.source |> Webhook.EventSource.toT, 52 | behavior: self.state.behavior |> Webhook.Behavior.fromString, 53 | payload: self.state.payload |> Webhook.PayloadType.fromString, 54 | }; 55 | self.send(ClearInputs); 56 | create(newWebhook); 57 | }; 58 | 59 | 60 | self.send(ChangeName(getVal(ev)))} 63 | /> 64 | 65 | 66 | self.send(ChangeUrl(getVal(ev)))} 69 | /> 70 | 71 | 72 | self.send(ChangeEvent(getVal(ev)))} 75 | /> 76 | 77 | 78 | self.send(ChangeSource(getVal(ev)))} 81 | /> 82 | 83 | 84 | self.send(ChangeBehavior(getVal(ev)))} 87 | /> 88 | 89 | 90 | self.send(ChangePayload(getVal(ev)))} 93 | /> 94 | 95 | 96 | 99 | 100 | ; 101 | }, 102 | }; -------------------------------------------------------------------------------- /src/cafeRouter.re: -------------------------------------------------------------------------------- 1 | type view = 2 | | Home 3 | | Order 4 | | Pay 5 | | AllOrders 6 | | Admin 7 | | Products 8 | | Config 9 | | Daily 10 | | Vendors 11 | | Cashiers 12 | | Webhooks 13 | | Discounts 14 | | Logs; 15 | 16 | type customerName = string; 17 | 18 | type state = {currentView: view}; 19 | 20 | type action = 21 | | Show(view); 22 | 23 | let component = ReasonReact.reducerComponent("CafeRouter"); 24 | 25 | let joinStrings = l => l |> Array.of_list |> Js.Array.joinWith(","); 26 | 27 | let make = _children => { 28 | ...component, 29 | initialState: () => {currentView: Home}, 30 | reducer: (action, _state) => 31 | switch (action) { 32 | | Show(view) => ReasonReact.Update({currentView: view}) 33 | }, 34 | didMount: self => { 35 | let watcherId = 36 | ReasonReact.Router.watchUrl(url => 37 | switch (url.path) { 38 | | [] => self.send(Show(Home)) 39 | | ["order"] => self.send(Show(Order)) 40 | | ["pay"] => self.send(Show(Pay)) 41 | | ["orders"] => self.send(Show(AllOrders)) 42 | | ["admin"] => self.send(Show(Admin)) 43 | | ["products"] => self.send(Show(Products)) 44 | | ["config"] => self.send(Show(Config)) 45 | | ["daily"] => self.send(Show(Daily)) 46 | | ["logs"] => self.send(Show(Logs)) 47 | | ["vendors"] => self.send(Show(Vendors)) 48 | | ["discounts"] => self.send(Show(Discounts)) 49 | | ["webhooks"] => self.send(Show(Webhooks)) 50 | | ["cashiers"] => self.send(Show(Cashiers)) 51 | | p => Js.log("I don't know this path. " ++ (p |> joinStrings)) 52 | } 53 | ); 54 | self.onUnmount(() => ReasonReact.Router.unwatchUrl(watcherId)); 55 | }, 56 | render: self => { 57 | let onStartNewOrder = customerName => 58 | ReasonReact.Router.push("order?customerName=" ++ customerName); 59 | let goHome = () => ReasonReact.Router.push("/"); 60 | let goToOrders = () => ReasonReact.Router.push("/orders"); 61 | let goToOrder = order => 62 | ReasonReact.Router.push( 63 | "/order?orderId=" ++ (order |> Order.fromVm).id, 64 | ); 65 | let queryString = ReasonReact.Router.dangerouslyGetInitialUrl().search; 66 | let orderId = 67 | switch (Util.QueryParam.get("orderId", queryString)) { 68 | | None => "" 69 | | Some(orderId) => orderId 70 | }; 71 | let startDate = ConfigurableDate.now() |> Date.startOfDay; 72 | let endDate = ConfigurableDate.now() |> Date.endOfDay; 73 |
74 | { 75 | switch (self.state.currentView) { 76 | | Home => 77 | | Order => 78 | | Pay => 79 | goHome()) 82 | onCancel=(o => goToOrder(o)) 83 | /> 84 | | AllOrders => 85 | | Admin => 86 | | Products => 87 | | Config => 88 | | Logs => 89 | | Vendors => 90 | | Cashiers => 91 | | Webhooks => 92 | | Discounts => 93 | | Daily => 94 | } 95 | } 96 |
; 97 | }, 98 | }; -------------------------------------------------------------------------------- /src/components/vendorManagement/VendorManagement.re: -------------------------------------------------------------------------------- 1 | open Js.Promise; 2 | 3 | type state = {vendors: list(Vendor.t)}; 4 | 5 | type action = 6 | | LoadVendors(list(Vendor.t)) 7 | | VendorRemoved(Vendor.t) 8 | | VendorModified(Vendor.t) 9 | | NewVendorCreated(Vendor.t); 10 | 11 | let component = ReasonReact.reducerComponent("VendorManagement"); 12 | 13 | let make = _children => { 14 | ...component, 15 | didMount: self => { 16 | VendorStore.getAll() 17 | |> Js.Promise.then_(vendors => { 18 | self.send(LoadVendors(vendors)); 19 | Js.Promise.resolve(); 20 | }) 21 | |> ignore; 22 | (); 23 | }, 24 | initialState: () => {vendors: []}, 25 | reducer: (action, state) => 26 | switch (action) { 27 | | LoadVendors(vendors) => ReasonReact.Update({vendors: vendors}) 28 | | VendorRemoved(exp) => 29 | ReasonReact.Update({ 30 | vendors: 31 | state.vendors |> List.filter((d: Vendor.t) => d.id !== exp.id), 32 | }) 33 | | VendorModified(exp) => 34 | ReasonReact.Update({ 35 | vendors: 36 | state.vendors 37 | |> List.map((d: Vendor.t) => d.id !== exp.id ? exp : d), 38 | }) 39 | | NewVendorCreated(exp) => 40 | ReasonReact.Update({vendors: List.concat([state.vendors, [exp]])}) 41 | }, 42 | render: self => { 43 | let goBack = _ => ReasonReact.Router.push("/admin"); 44 | let removeVendor = (p: Vendor.t) => { 45 | VendorStore.remove(~id=p.id) 46 | |> then_(_ => { 47 | self.send(VendorRemoved(p)); 48 | resolve(); 49 | }) 50 | |> ignore; 51 | (); 52 | }; 53 | let modifyVendor = (modifiedVendor: Vendor.t) => 54 | VendorStore.update(modifiedVendor) 55 | |> then_(_ => { 56 | self.send(VendorModified(modifiedVendor)); 57 | resolve(); 58 | }) 59 | |> ignore; 60 | let createVendor = (newVendor: Vendor.New.t) => { 61 | VendorStore.add(newVendor) 62 | |> Js.Promise.then_((newVendor: Vendor.t) => { 63 | self.send(NewVendorCreated(newVendor)); 64 | Js.Promise.resolve(); 65 | }) 66 | |> ignore; 67 | (); 68 | }; 69 |
70 |
71 |
72 |
79 |
80 | (ReactUtils.sloc("admin.vendors.header")) 81 |
82 |
83 |
84 | 85 | 86 | 87 | 88 | 89 | ( 90 | self.state.vendors 91 | |> List.map((d: Vendor.t) => 92 | 98 | ) 99 | |> Array.of_list 100 | |> ReasonReact.array 101 | ) 102 | 103 | 104 |
(ReactUtils.sloc("vendor.name"))
105 |
106 |
; 107 | }, 108 | }; -------------------------------------------------------------------------------- /src/components/vendorManagement/VendorManagementRow.re: -------------------------------------------------------------------------------- 1 | open ReactUtils; 2 | 3 | type state = { 4 | modifying: bool, 5 | showModal: bool, 6 | modifiedVendor: Vendor.t, 7 | originalVendor: Vendor.t, 8 | name: string, 9 | }; 10 | 11 | type action = 12 | | EnableMod 13 | | CancelMod 14 | | ShowDialog 15 | | HideDialog 16 | | SaveMod(Vendor.t) 17 | | ChangeName(string); 18 | 19 | let component = ReasonReact.reducerComponent("VendorManagementRow"); 20 | 21 | let make = (~vendor, ~remove, ~modify, _children) => { 22 | ...component, 23 | initialState: () => { 24 | modifying: false, 25 | originalVendor: vendor, 26 | modifiedVendor: vendor, 27 | name: vendor.name, 28 | showModal: false, 29 | }, 30 | reducer: (action, state) => 31 | switch (action) { 32 | | EnableMod => ReasonReact.Update({...state, modifying: true}) 33 | | CancelMod => 34 | ReasonReact.Update({ 35 | ...state, 36 | modifying: false, 37 | modifiedVendor: state.originalVendor, 38 | }) 39 | | SaveMod(vendor) => 40 | ReasonReact.Update({ 41 | ...state, 42 | modifying: false, 43 | originalVendor: vendor, 44 | modifiedVendor: vendor, 45 | }) 46 | | ShowDialog => ReasonReact.Update({...state, showModal: true}) 47 | | HideDialog => ReasonReact.Update({...state, showModal: false}) 48 | | ChangeName(newVal) => 49 | ReasonReact.Update({ 50 | ...state, 51 | modifiedVendor: { 52 | ...state.modifiedVendor, 53 | name: newVal, 54 | }, 55 | }) 56 | }, 57 | render: self => { 58 | let saveModification = _ => { 59 | let modified = self.state.modifiedVendor; 60 | modify(modified); 61 | self.send(SaveMod(modified)); 62 | }; 63 |
64 | remove(self.state.originalVendor)} 69 | onCancel={() => self.send(HideDialog)} 70 | /> 71 | { 72 | switch (self.state.modifying) { 73 | | false => 74 | 75 | 76 |
; 118 | }, 119 | }; -------------------------------------------------------------------------------- /src/components/discounts/DiscountEdit.re: -------------------------------------------------------------------------------- 1 | module DiscountFormParams = { 2 | type state = { 3 | name: string, 4 | percent: string, 5 | }; 6 | type fields = [ | `name | `percent | `price | `taxCalculation | `tags]; 7 | let lens = [ 8 | (`name, s => s.name, (s, name) => {...s, name}), 9 | (`percent, s => s.percent, (s, percent) => {...s, percent}), 10 | ]; 11 | }; 12 | 13 | let validationMessage = message => 14 | switch (message) { 15 | | None => ReasonReact.null 16 | | Some(msg) => {ReactUtils.sloc(msg)} 17 | }; 18 | 19 | module EditDiscountForm = ReForm.Create(DiscountFormParams); 20 | 21 | let component = ReasonReact.statelessComponent("DiscountEdit"); 22 | 23 | let make = 24 | ( 25 | ~discount: option(Discount.t)=None, 26 | ~onCancel=() => (), 27 | ~onSubmit, 28 | ~discounts, 29 | _children, 30 | ) => { 31 | ...component, 32 | render: _self => { 33 | let hasDuplicateName = name => { 34 | let duplicates = 35 | discounts |> List.filter((c: Discount.t) => c.name === name); 36 | let isDuplicate = duplicates |> List.length > 0; 37 | isDuplicate ? Some("validation.duplicate") : None; 38 | }; 39 | let isUnique = (original: option(Discount.t), new_) => 40 | switch (original) { 41 | | None => hasDuplicateName(new_) 42 | | Some(disc) => 43 | if (disc.name === new_) { 44 | None; 45 | } else { 46 | hasDuplicateName(new_); 47 | } 48 | }; 49 | {name: "", percent: ""} 54 | | Some(disc) => { 55 | name: disc.name, 56 | percent: disc.percent |> Percent.toDisplay, 57 | } 58 | } 59 | } 60 | schema=[ 61 | (`name, Custom(v => v.name |> isUnique(discount))), 62 | (`percent, Required), 63 | ]> 64 | ...{ 65 | ({handleSubmit, handleChange, form, getErrorForField}) => { 66 | let field = (label, value, fieldType: DiscountFormParams.fields) => 67 |
68 | 79 | {validationMessage(getErrorForField(fieldType))} 80 |
; 81 |
83 | {field("discount.name", form.values.name, `name)} 84 | {field("discount.percent", form.values.percent, `percent)} 85 |
86 |
99 |
; 100 | } 101 | } 102 |
; 103 | }, 104 | }; -------------------------------------------------------------------------------- /src/components/orderTaking/ClosedOrderInfo.re: -------------------------------------------------------------------------------- 1 | open ReactUtils; 2 | 3 | let component = ReasonReact.statelessComponent("ClosedOrderInfo"); 4 | let language = Config.App.get().language; 5 | let make = (~order: Order.orderVm, ~paidDateChanged, _children) => { 6 | ...component, 7 | render: _self => 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | 25 | { 26 | switch (order.paid) { 27 | | None => 28 | | Some(paid) => 29 | 30 | 31 | 32 | 33 | 34 | 35 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | { 64 | switch (paid.externalId) { 65 | | "" => ReasonReact.null 66 | | id => 67 | 68 | 69 | 70 | 71 | } 72 | } 73 | 74 | } 75 | } 76 | { 77 | switch (order.returned) { 78 | | None => 79 | | Some(returned) => 80 | 81 | 82 | 85 | 86 | 87 | 88 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | } 102 | } 103 |

{sloc("order.created.header")}

{sloc("order.created.date")} 17 | { 18 | language === "EN" ? 19 | s(order.createdOn |> Date.toDisplayEN) : 20 | s(order.createdOn |> Date.toDisplay) 21 | } 22 |

{sloc("order.paid.header")}

{sloc("order.paid.date")} 36 | { 37 | switch (order.returned) { 38 | | None => 39 | Js.String.toLowerCase} 41 | value={paid.on |> Js.Date.fromFloat} 42 | onChange=( 43 | moment => 44 | paidDateChanged(moment |> MomentRe.Moment.valueOf) 45 | ) 46 | /> 47 | | Some(_) => 48 | language === "EN" ? 49 | s(paid.on |> Date.toDisplayEN) : 50 | s(paid.on |> Date.toDisplay) 51 | } 52 | } 53 |
{sloc("order.paid.by")} {s(paid.by)}
{sloc("paymentMethod")} {sloc(paid.method.name)}
{sloc(paid.method.name ++ ".externalId")} {s(id)}
83 |

{sloc("order.returned.header")}

84 |
{sloc("order.returned.date")} 89 | { 90 | language === "EN" ? 91 | s(returned.on |> Date.toDisplayEN) : 92 | s(returned.on |> Date.toDisplay) 93 | } 94 |
{sloc("order.returned.by")} {s(returned.by)}
104 |
, 115 | }; -------------------------------------------------------------------------------- /src/components/orderTaking/KeyInput.re: -------------------------------------------------------------------------------- 1 | [@bs.val] external window: Dom.window = ""; 2 | 3 | [@bs.send] 4 | external addEventListener: (Dom.window, string, 'a => unit) => unit = ""; 5 | 6 | [@bs.send] 7 | external removeEventListener: (Dom.window, string, 'a => unit) => unit = ""; 8 | 9 | type state = {value: string}; 10 | 11 | type action = 12 | | Reset 13 | | KeyDown(int); 14 | 15 | let component = ReasonReact.reducerComponent("KeyInput"); 16 | 17 | let stringOrDefault = (opt: option(string)) => 18 | switch (opt) { 19 | | None => "" 20 | | Some(s) => s 21 | }; 22 | 23 | let make = 24 | ( 25 | ~onCancel=() => (), 26 | ~acceptInput=true, 27 | ~onFinish, 28 | ~className="", 29 | _children, 30 | ) => { 31 | ...component, 32 | initialState: () => {value: ""}, 33 | reducer: (action, state) => 34 | switch (action) { 35 | | KeyDown(27) => 36 | ReasonReact.UpdateWithSideEffects({value: ""}, (_ => onCancel())) 37 | | KeyDown(8) => 38 | ReasonReact.Update({ 39 | value: Js.String.slice(~from=0, ~to_=-1, state.value), 40 | }) 41 | | KeyDown(20) => ReasonReact.Update({value: state.value}) 42 | | KeyDown(112) => ReasonReact.Update({value: state.value}) 43 | | KeyDown(113) => ReasonReact.Update({value: state.value}) 44 | | KeyDown(114) => ReasonReact.Update({value: state.value}) 45 | | KeyDown(115) => ReasonReact.Update({value: state.value}) 46 | | KeyDown(116) => ReasonReact.Update({value: state.value}) 47 | | KeyDown(117) => ReasonReact.Update({value: state.value}) 48 | | KeyDown(118) => ReasonReact.Update({value: state.value}) 49 | | KeyDown(119) => ReasonReact.Update({value: state.value}) 50 | | KeyDown(120) => ReasonReact.Update({value: state.value}) 51 | | KeyDown(121) => ReasonReact.Update({value: state.value}) 52 | | KeyDown(122) => ReasonReact.Update({value: state.value}) 53 | | KeyDown(123) => ReasonReact.Update({value: state.value}) 54 | | KeyDown(17) => ReasonReact.Update({value: state.value}) 55 | | KeyDown(38) => ReasonReact.Update({value: state.value}) 56 | | KeyDown(39) => ReasonReact.Update({value: state.value}) 57 | | KeyDown(37) => ReasonReact.Update({value: state.value}) 58 | | KeyDown(40) => ReasonReact.Update({value: state.value}) 59 | | KeyDown(45) => ReasonReact.Update({value: state.value}) 60 | | KeyDown(46) => ReasonReact.Update({value: state.value}) 61 | | KeyDown(18) => ReasonReact.Update({value: state.value}) 62 | | KeyDown(191) => ReasonReact.Update({value: state.value}) 63 | | KeyDown(192) => ReasonReact.Update({value: state.value}) 64 | | KeyDown(188) => ReasonReact.Update({value: state.value}) 65 | | KeyDown(190) => ReasonReact.Update({value: state.value}) 66 | | KeyDown(16) => ReasonReact.Update({value: state.value}) 67 | | KeyDown(35) => ReasonReact.Update({value: state.value}) 68 | | KeyDown(186) => ReasonReact.Update({value: state.value}) 69 | | KeyDown(189) => ReasonReact.Update({value: state.value}) 70 | | KeyDown(187) => ReasonReact.Update({value: state.value}) 71 | | KeyDown(219) => ReasonReact.Update({value: state.value}) 72 | | KeyDown(220) => ReasonReact.Update({value: state.value}) 73 | | KeyDown(221) => ReasonReact.Update({value: state.value}) 74 | | KeyDown(222) => ReasonReact.Update({value: state.value}) 75 | | KeyDown(13) => 76 | ReasonReact.UpdateWithSideEffects( 77 | {value: ""}, 78 | (_ => onFinish(state.value)), 79 | ) 80 | | KeyDown(key) => 81 | ReasonReact.Update({ 82 | value: 83 | acceptInput ? 84 | state.value ++ (key |> Js.String.fromCharCode) : state.value, 85 | }) 86 | | Reset => ReasonReact.Update({value: ""}) 87 | }, 88 | didMount: self => { 89 | let logKey = ev => self.send(KeyDown(ReactEvent.Keyboard.which(ev))); 90 | addEventListener(window, "keydown", logKey); 91 | self.onUnmount(() => removeEventListener(window, "keydown", logKey)); 92 | }, 93 | render: self => 94 | , 99 | }; -------------------------------------------------------------------------------- /src/components/webhooks/WebhookManagement.re: -------------------------------------------------------------------------------- 1 | open Js.Promise; 2 | 3 | type intent = 4 | | Viewing 5 | | Deleting(Webhook.t); 6 | 7 | type state = { 8 | webhooks: list(Webhook.t), 9 | intent, 10 | }; 11 | 12 | type action = 13 | | LoadWebhooks(list(Webhook.t)) 14 | | WebhookRemoved(Webhook.t) 15 | | ShowDialog(Webhook.t) 16 | | HideDialog 17 | | NewWebhookCreated(Webhook.t); 18 | 19 | let component = ReasonReact.reducerComponent("WebhookManagement"); 20 | 21 | let make = _children => { 22 | ...component, 23 | didMount: self => 24 | WebhookStore.getAll() 25 | |> Js.Promise.then_(webhooks => { 26 | self.send(LoadWebhooks(webhooks)); 27 | Js.Promise.resolve(); 28 | }) 29 | |> ignore, 30 | initialState: () => {webhooks: [], intent: Viewing}, 31 | reducer: (action, state) => 32 | switch (action) { 33 | | LoadWebhooks(webhooks) => ReasonReact.Update({...state, webhooks}) 34 | | ShowDialog(dis) => 35 | ReasonReact.Update({...state, intent: Deleting(dis)}) 36 | | HideDialog => ReasonReact.Update({...state, intent: Viewing}) 37 | | WebhookRemoved(dis) => 38 | ReasonReact.Update({ 39 | intent: Viewing, 40 | webhooks: 41 | state.webhooks |> List.filter((d: Webhook.t) => d.id !== dis.id), 42 | }) 43 | | NewWebhookCreated(dis) => 44 | ReasonReact.Update({ 45 | ...state, 46 | webhooks: List.concat([state.webhooks, [dis]]), 47 | }) 48 | }, 49 | render: self => { 50 | let goBack = _ => ReasonReact.Router.push("/admin"); 51 | let displayDialog = (p: Webhook.t) => self.send(ShowDialog(p)); 52 | let removeWebhook = (p: Webhook.t) => { 53 | WebhookStore.remove(~id=p.id) 54 | |> then_(_ => { 55 | self.send(WebhookRemoved(p)); 56 | resolve(); 57 | }) 58 | |> ignore; 59 | (); 60 | }; 61 | let createWebhook = (newWebhook: Webhook.New.t) => { 62 | WebhookStore.add(newWebhook) 63 | |> Js.Promise.then_((newWebhook: Webhook.t) => { 64 | self.send(NewWebhookCreated(newWebhook)); 65 | Js.Promise.resolve(); 66 | }) 67 | |> ignore; 68 | (); 69 | }; 70 |
71 | { 72 | switch (self.state.intent) { 73 | | Deleting(dis) => 74 | removeWebhook(dis)) 79 | onCancel=(() => self.send(HideDialog)) 80 | /> 81 | | Viewing => 82 |
83 |
84 |
85 |
86 | {ReactUtils.s("Atras")} 87 |
88 |
89 |
90 | {ReactUtils.s("Gestion de Webhooks")} 91 |
92 |
93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 105 | 106 | 107 | { 108 | self.state.webhooks 109 | |> List.map((d: Webhook.t) => 110 | 115 | ) 116 | |> Array.of_list 117 | |> ReasonReact.array 118 | } 119 | 120 | 121 |
{ReactUtils.s("Nombre")} {ReactUtils.s("Url")} {ReactUtils.s("Evento")} {ReactUtils.s("Fuente")} {ReactUtils.s("Comportamiento")} {ReactUtils.s("Tipo")} 104 |
122 |
123 |
124 | } 125 | } 126 |
; 127 | }, 128 | }; -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | } else { 39 | // Is not local host. Just register service worker 40 | registerValidSW(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/components/orderTaking/orderActions.re: -------------------------------------------------------------------------------- 1 | [@bs.val] external alert : string => unit = ""; 2 | open OrderHelper; 3 | 4 | type userIntent = 5 | | Building 6 | | Returning; 7 | 8 | type state = { 9 | userIntent, 10 | showModal: bool, 11 | }; 12 | 13 | type action = 14 | | ChangeIntent(userIntent) 15 | | SaveAndExit 16 | | SaveAndGoToPayScreen 17 | | ShowDialog 18 | | HideDialog 19 | | DeleteAndExit 20 | | ReturnAndExit(Cashier.t); 21 | 22 | let component = ReasonReact.reducerComponent("OrderActions"); 23 | 24 | let stringOrDefault = (opt: option(string)) => 25 | switch (opt) { 26 | | None => "" 27 | | Some(s) => s 28 | }; 29 | 30 | let make = 31 | ( 32 | ~order: Order.orderVm, 33 | ~onShowProductModal=() => (), 34 | ~onFinish, 35 | _children, 36 | ) => { 37 | ...component, 38 | initialState: () => {userIntent: Building, showModal: false}, 39 | reducer: (action, state) => 40 | switch (action) { 41 | | ChangeIntent(intent) => 42 | ReasonReact.Update({...state, userIntent: intent}) 43 | | SaveAndExit => 44 | ReasonReact.SideEffects((_self => saveOrder(order, onFinish))) 45 | | ReturnAndExit(cashier) => 46 | ReasonReact.SideEffects( 47 | (_self => returnOrder(cashier, order, onFinish)), 48 | ) 49 | | ShowDialog => ReasonReact.Update({...state, showModal: true}) 50 | | HideDialog => ReasonReact.Update({...state, showModal: false}) 51 | | DeleteAndExit => 52 | ReasonReact.SideEffects((_self => removeOrder(order, onFinish))) 53 | | SaveAndGoToPayScreen => 54 | ReasonReact.SideEffects( 55 | ( 56 | _self => 57 | saveOrder(order, o => 58 | ReasonReact.Router.push( 59 | "/pay?orderId=" ++ stringOrDefault(o.id), 60 | ) 61 | ) 62 | ), 63 | ) 64 | }, 65 | render: self => { 66 | let items = order.orderItems |> Array.of_list; 67 | let disablePayButton = items |> Array.length === 0; 68 | let saveButton = 69 | 178 | ) 179 | |> Array.of_list 180 | |> ReasonReact.array 181 | ) 182 | ; 183 | }, 184 | }; -------------------------------------------------------------------------------- /src/components/products/ProductEdit.re: -------------------------------------------------------------------------------- 1 | module ProductFormParams = { 2 | type state = { 3 | name: string, 4 | sku: string, 5 | price: string, 6 | taxCalculationMethod: string, 7 | taxRate: string, 8 | tags: string, 9 | }; 10 | type fields = [ 11 | | `name 12 | | `sku 13 | | `price 14 | | `taxCalculationMethod 15 | | `taxRate 16 | | `tags 17 | ]; 18 | let lens = [ 19 | (`name, s => s.name, (s, name) => {...s, name}), 20 | (`sku, s => s.sku, (s, sku) => {...s, sku}), 21 | (`price, s => s.price, (s, price) => {...s, price}), 22 | ( 23 | `taxCalculationMethod, 24 | s => s.taxCalculationMethod, 25 | (s, taxCalculationMethod) => {...s, taxCalculationMethod}, 26 | ), 27 | (`taxRate, s => s.taxRate, (s, taxRate) => {...s, taxRate}), 28 | (`tags, s => s.tags, (s, tags) => {...s, tags}), 29 | ]; 30 | }; 31 | 32 | let validationMessage = message => 33 | switch (message) { 34 | | None => ReasonReact.null 35 | | Some(msg) => {ReactUtils.sloc(msg)} 36 | }; 37 | 38 | module EditProductForm = ReForm.Create(ProductFormParams); 39 | 40 | let defaultTaxCalculationMethod = "totalFirst"; 41 | 42 | let component = ReasonReact.statelessComponent("ProductEdit"); 43 | 44 | let make = 45 | ( 46 | ~product: option(Product.t)=None, 47 | ~onCancel=() => (), 48 | ~onSubmit, 49 | ~products, 50 | _children, 51 | ) => { 52 | ...component, 53 | render: _self => { 54 | let hasDuplicateSku = sku => { 55 | let duplicates = 56 | products |> List.filter((c: Product.t) => c.sku === sku); 57 | let isDuplicate = duplicates |> List.length > 0; 58 | isDuplicate ? Some("validation.duplicate") : None; 59 | }; 60 | let isUnique = (original: option(Product.t), new_) => 61 | switch (original) { 62 | | None => hasDuplicateSku(new_) 63 | | Some(prod) => 64 | if (prod.sku === new_) { 65 | None; 66 | } else { 67 | hasDuplicateSku(new_); 68 | } 69 | }; 70 | let canAddRate = (taxCalculationMethod, taxRate) => 71 | if (taxCalculationMethod !== "exempt" && taxRate === "") { 72 | Some("validation.required"); 73 | } else { 74 | None; 75 | }; 76 | { 81 | name: "", 82 | sku: "", 83 | price: "", 84 | taxCalculationMethod: defaultTaxCalculationMethod, 85 | taxRate: "", 86 | tags: "", 87 | } 88 | | Some(prod) => 89 | let [|taxCalculationMethod, taxRate|] = 90 | Js.String.splitAtMost( 91 | "|", 92 | ~limit=2, 93 | prod.taxCalculation |> Tax.Calculation.toDelimitedString, 94 | ); 95 | { 96 | name: prod.name, 97 | sku: prod.sku, 98 | price: prod.suggestedPrice |> Money.toDisplay, 99 | taxCalculationMethod, 100 | taxRate, 101 | tags: prod.tags |> Tags.toCSV, 102 | }; 103 | } 104 | } 105 | schema=[ 106 | (`name, Required), 107 | (`sku, Custom(v => v.sku |> isUnique(product))), 108 | (`price, Required), 109 | (`taxCalculationMethod, Required), 110 | ( 111 | `taxRate, 112 | Custom(v => canAddRate(v.taxCalculationMethod, v.taxRate)), 113 | ), 114 | (`tags, Required), 115 | ]> 116 | ...{ 117 | ({handleSubmit, handleChange, form, getErrorForField}) => { 118 | let field = (label, value, fieldType: ProductFormParams.fields) => 119 |
120 | 131 | {validationMessage(getErrorForField(fieldType))} 132 |
; 133 |
135 | {field("product.name", form.values.name, `name)} 136 | {field("product.sku", form.values.sku, `sku)} 137 | {field("product.price", form.values.price, `price)} 138 |
139 | 159 | {validationMessage(getErrorForField(`taxCalculationMethod))} 160 |
161 | { 162 | if (form.values.taxCalculationMethod !== "exempt") { 163 |
164 | 178 | {validationMessage(getErrorForField(`taxRate))} 179 |
; 180 | } else { 181 | ReasonReact.null; 182 | } 183 | } 184 | {field("product.tags", form.values.tags, `tags)} 185 |
186 |
187 |
200 | ; 201 | } 202 | } 203 | ; 204 | }, 205 | }; -------------------------------------------------------------------------------- /src/components/shared/ItemManager.re: -------------------------------------------------------------------------------- 1 | module Create = (ItemStore: DbStore.T) => { 2 | type columnRenderer = { 3 | nameKey: string, 4 | render: ItemStore.item => ReasonReact.reactElement, 5 | }; 6 | 7 | type intent = 8 | | Viewing 9 | | Creating 10 | | Updating(ItemStore.item) 11 | | Deleting(ItemStore.item); 12 | 13 | type state = { 14 | items: list(ItemStore.item), 15 | intent, 16 | }; 17 | 18 | type action = 19 | | UpdateIntent(intent) 20 | | LoadItems 21 | | ItemsLoaded(list(ItemStore.item)) 22 | | DeleteItem(ItemStore.item) 23 | | ItemDeleted(ItemStore.item) 24 | | UpdateItem(ItemStore.item) 25 | | ItemUpdated(ItemStore.item) 26 | | CreateItem(ItemStore.newItem) 27 | | ItemCreated(ItemStore.item); 28 | 29 | let component = ReasonReact.reducerComponent("ItemManager"); 30 | 31 | let goBack = _ => ReasonReact.Router.push("/admin"); 32 | 33 | let make = 34 | ( 35 | ~headerKey, 36 | ~name, 37 | ~renderCreate, 38 | ~renderEdit, 39 | ~renderColumns, 40 | _children, 41 | ) => { 42 | ...component, 43 | initialState: () => {items: [], intent: Viewing}, 44 | didMount: self => self.send(LoadItems), 45 | reducer: (action, state) => 46 | switch (action) { 47 | | UpdateIntent(intent) => ReasonReact.Update({...state, intent}) 48 | | LoadItems => 49 | ReasonReact.SideEffects( 50 | ( 51 | self => 52 | Js.Promise.( 53 | ItemStore.getAll() 54 | |> then_(items => resolve(self.send(ItemsLoaded(items)))) 55 | |> ignore 56 | ) 57 | ), 58 | ) 59 | | ItemsLoaded(items) => ReasonReact.Update({...state, items}) 60 | | DeleteItem(item) => 61 | ReasonReact.UpdateWithSideEffects( 62 | {...state, intent: Viewing}, 63 | ( 64 | self => 65 | Js.Promise.( 66 | ItemStore.remove(~id=ItemStore.id(item)) 67 | |> then_(() => resolve(self.send(ItemDeleted(item)))) 68 | |> ignore 69 | ) 70 | ), 71 | ) 72 | | ItemDeleted(item) => 73 | ReasonReact.Update({ 74 | ...state, 75 | items: 76 | state.items 77 | |> List.filter(i => ItemStore.id(i) !== ItemStore.id(item)), 78 | }) 79 | | UpdateItem(item) => 80 | ReasonReact.UpdateWithSideEffects( 81 | {...state, intent: Viewing}, 82 | ( 83 | self => 84 | Js.Promise.( 85 | ItemStore.update(item) 86 | |> then_(item => resolve(self.send(ItemUpdated(item)))) 87 | |> catch(err => resolve(Js.log(err))) 88 | |> ignore 89 | ) 90 | ), 91 | ) 92 | | ItemUpdated(item) => 93 | ReasonReact.Update({ 94 | ...state, 95 | items: 96 | state.items 97 | |> List.map(i => 98 | ItemStore.id(i) === ItemStore.id(item) ? item : i 99 | ), 100 | }) 101 | | CreateItem(item) => 102 | ReasonReact.UpdateWithSideEffects( 103 | {...state, intent: Viewing}, 104 | ( 105 | self => 106 | Js.Promise.( 107 | ItemStore.add(item) 108 | |> then_(item => resolve(self.send(ItemCreated(item)))) 109 | |> ignore 110 | ) 111 | ), 112 | ) 113 | | ItemCreated(item) => 114 | ReasonReact.Update({...state, items: state.items @ [item]}) 115 | }, 116 | render: self => { 117 | let lowerCaseName = String.lowercase(name); 118 | let capitalizedName = String.capitalize(lowerCaseName); 119 |
120 |
121 |
122 |
self.send(UpdateIntent(Creating)))> 125 | (ReactUtils.s("Create")) 126 |
127 |
128 | (ReactUtils.s("Atras")) 129 |
130 |
131 |
(ReactUtils.sloc(headerKey))
132 |
133 |
134 | 135 | 136 | 137 | 144 | ) 145 | |> ReasonReact.array 146 | ) 147 | 149 | 150 | 151 | ( 152 | self.state.items 153 | |> List.map(item => 154 | 155 | 165 | ( 166 | renderColumns 167 | |> Array.mapi((i, columnRenderer) => 168 | 171 | ) 172 | |> ReasonReact.array 173 | ) 174 | 184 | 185 | ) 186 | |> Array.of_list 187 | |> ReasonReact.array 188 | ) 189 | 190 |
138 | ( 139 | renderColumns 140 | |> Array.mapi((i, {nameKey}) => 141 | string_of_int)> 142 | (ReactUtils.sloc(nameKey)) 143 | 148 |
156 | string_of_int)> 169 | (columnRenderer.render(item)) 170 | 175 |
191 |
192 | ( 193 | switch (self.state.intent) { 194 | | Viewing => ReasonReact.null 195 | | Creating => 196 | self.send(UpdateIntent(Viewing))) 200 | render=( 201 | () => 202 | renderCreate( 203 | ~items=self.state.items, 204 | ~onSubmit=item => self.send(CreateItem(item)), 205 | ~onCancel=_ => self.send(UpdateIntent(Viewing)), 206 | ) 207 | ) 208 | /> 209 | | Updating(item) => 210 | self.send(UpdateIntent(Viewing))) 214 | render=( 215 | () => 216 | renderEdit( 217 | ~items=self.state.items, 218 | ~item, 219 | ~onSubmit=item => self.send(UpdateItem(item)), 220 | ~onCancel=_ => self.send(UpdateIntent(Viewing)), 221 | ) 222 | ) 223 | /> 224 | | Deleting(item) => 225 | self.send(DeleteItem(item))) 230 | onCancel=(() => self.send(UpdateIntent(Viewing))) 231 | /> 232 | } 233 | ) 234 |
; 235 | }, 236 | }; 237 | }; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | font-size: 15px; 6 | } 7 | 8 | .header { 9 | background-color: #ccc; 10 | min-height: 65px; 11 | margin-bottom: 15px; 12 | padding: 10px; 13 | } 14 | 15 | .header-options { 16 | padding: 15px; 17 | font-size: 30px; 18 | } 19 | .spaceDivider { 20 | width: 15px; 21 | height: auto; 22 | display: inline-block; 23 | } 24 | .quantityDivider { 25 | width: 0px; 26 | height: auto; 27 | display: inline-block; 28 | } 29 | .heightDivider { 30 | height: 15px; 31 | } 32 | .quantitySelectorDiv { 33 | display: inline-block; 34 | } 35 | .quantityInput { 36 | width: 15%; 37 | } 38 | .quantityMinusButton { 39 | position: "top-right"; 40 | } 41 | .modal { 42 | position: fixed; 43 | z-index: 1; 44 | left: 0; 45 | top: 0; 46 | width: 100%; 47 | height: 100%; 48 | overflow: auto; 49 | background-color: rgb(0, 0, 0); 50 | background-color: rgba(0, 0, 0, 4); 51 | } 52 | .alert { 53 | position: fixed; 54 | z-index: 1; 55 | width: 20%; 56 | height: 20%; 57 | left: 0; 58 | right: 0; 59 | border: 1px solid #666; 60 | background-color: darkgray; 61 | } 62 | .modal-header { 63 | text-align: center; 64 | font-size: 40px; 65 | } 66 | .modal-footer { 67 | margin-top: 20px; 68 | text-align: center; 69 | } 70 | 71 | .modal-body { 72 | background-color: #fefefe; 73 | margin: 0 auto; 74 | width: 65%; 75 | font-size: 20px; 76 | } 77 | 78 | .modal-content { 79 | background-color: #fefefe; 80 | margin: 15% auto; 81 | padding: 20px; 82 | width: 65%; 83 | font-size: 20px; 84 | position: relative; 85 | animation-name: animatetop; 86 | animation-duration: 0.4s; 87 | } 88 | @keyframes animatetop { 89 | from { 90 | top: -300px; 91 | opacity: 0; 92 | } 93 | to { 94 | top: 0; 95 | opacity: 1; 96 | } 97 | } 98 | .header-menu { 99 | float: right; 100 | /* width: 150px; */ 101 | } 102 | 103 | input.big { 104 | font-size: 30px; 105 | } 106 | input.pin { 107 | width: 70px; 108 | } 109 | 110 | input.big-text { 111 | font-size: 30px; 112 | width: 100%; 113 | } 114 | 115 | input.customer-name-input { 116 | font-size: 30px; 117 | width: 300px; 118 | } 119 | .paid-date input { 120 | font-size: 20px; 121 | width: 400px; 122 | } 123 | .paid-date button { 124 | font-size: 20px; 125 | margin-left: 10px; 126 | } 127 | 128 | .open-orders { 129 | margin: 10px; 130 | } 131 | 132 | .open-order-card .time { 133 | font-size: 11px; 134 | color: darkblue; 135 | margin-top: 5px; 136 | } 137 | .open-order-card .customer-name { 138 | color: darkblue; 139 | font-size: 25px; 140 | margin-bottom: 5px; 141 | } 142 | 143 | .sub-label { 144 | font-size: 14px; 145 | color: rgba(0, 0, 0, 0.6); 146 | border-top: 1px solid #36373c; 147 | } 148 | 149 | .pin-entry { 150 | border: 1px solid black; 151 | border-radius: 8px; 152 | margin: 0 10px 0 10px; 153 | display: inline-block; 154 | 155 | text-align: center; 156 | font-size: 18px; 157 | padding: 0 10px 0 10px; 158 | } 159 | .pin-entry input { 160 | font-size: 25px; 161 | } 162 | .search-input { 163 | width: 75%; 164 | font-size: 20px; 165 | box-sizing: border-box; 166 | background-color: white; 167 | height: 55%; 168 | } 169 | .card { 170 | border: 0px; 171 | border-radius: 8px; 172 | padding: 10px; 173 | margin: 10px; 174 | background-color: lightblue; 175 | display: inline-block; 176 | 177 | text-align: center; 178 | cursor: pointer; 179 | font-size: 18px; 180 | } 181 | 182 | .quiet-card { 183 | background-color: #888; 184 | color: #444; 185 | } 186 | .wide-card { 187 | width: 100px; 188 | } 189 | .small-card { 190 | padding: 5px; 191 | margin: 5px; 192 | font-size: 13px; 193 | } 194 | .smallItems-card { 195 | border-radius: 35%; 196 | height: 5; 197 | width: 10; 198 | margin-right: 10%; 199 | justify-content: center; 200 | font-size: 14px; 201 | } 202 | .danger-card { 203 | background-color: darkred; 204 | color: #dddddd; 205 | } 206 | .exit-card { 207 | background-color: white; 208 | } 209 | .order .right-side { 210 | width: 475px; 211 | background-color: #bbbbbb; 212 | float: right; 213 | padding: 10px; 214 | } 215 | 216 | .back-button-card { 217 | background-color: blueviolet; 218 | color: white; 219 | } 220 | 221 | .order-items table { 222 | width: 100%; 223 | border-collapse: collapse; 224 | text-align: left; 225 | } 226 | .order-items table tbody td { 227 | height: 30px; 228 | padding-left: 5px; 229 | padding-right: 5px; 230 | } 231 | .order-items table tbody tr.note-row td ul { 232 | list-style-type: square; 233 | margin: 0; 234 | } 235 | .order-items table tfoot th { 236 | height: 30px; 237 | text-align: right; 238 | padding-right: 10px; 239 | } 240 | .order-items table tfoot td { 241 | width: 75px; 242 | } 243 | .order-items table tfoot tr.divider td { 244 | border-top: 2px solid black; 245 | } 246 | .order-items table tfoot tr.divider th { 247 | border-top: 2px solid black; 248 | } 249 | .order-items td.quantity { 250 | width: 130px; 251 | } 252 | 253 | .product-card { 254 | background-color: goldenrod; 255 | } 256 | .tag-card { 257 | background-color: cornflowerblue; 258 | color: white; 259 | } 260 | 261 | .order-header { 262 | background-color: #ccc; 263 | padding: 15px; 264 | height: 40px; 265 | } 266 | .order-header .customer-name { 267 | font-size: 30px; 268 | width: 350px; 269 | } 270 | .order-actions { 271 | float: right; 272 | } 273 | .order-actions .card { 274 | margin-top: 0px; 275 | margin-bottom: 0px; 276 | } 277 | .productFields { 278 | padding-right: 10px; 279 | } 280 | .exit-modal-button-card { 281 | position: absolute; 282 | top: 0; 283 | right: 0; 284 | } 285 | .save-button-card { 286 | background-color: darkblue; 287 | color: white; 288 | 289 | margin-top: 0px; 290 | margin-bottom: 0px; 291 | } 292 | .pay-button-card { 293 | background-color: green; 294 | color: white; 295 | margin-top: 0px; 296 | margin-bottom: 0px; 297 | } 298 | .remove-button-card { 299 | background-color: darkred; 300 | color: white; 301 | 302 | margin-top: 0px; 303 | margin-bottom: 0px; 304 | } 305 | .cancel-button-card { 306 | background-color: gray; 307 | color: black; 308 | 309 | margin-top: 0px; 310 | margin-bottom: 0px; 311 | } 312 | table.order-list { 313 | width: 100%; 314 | } 315 | 316 | .valid { 317 | color: darkgreen; 318 | } 319 | .invalid { 320 | color: darkred; 321 | } 322 | 323 | table.admin-table { 324 | width: 100%; 325 | border-collapse: collapse; 326 | text-align: left; 327 | } 328 | 329 | table.admin-table thead th { 330 | background-color: #bbbbbb; 331 | padding: 5px 0 5px 0; 332 | } 333 | 334 | .discounts { 335 | clear: left; 336 | margin-left: 5px; 337 | } 338 | .card-discount { 339 | background-color: #ccc; 340 | } 341 | 342 | .no-float { 343 | clear: both; 344 | } 345 | 346 | .bulk-import { 347 | width: 100%; 348 | height: 100px; 349 | } 350 | .invalid { 351 | font-size: 10px; 352 | padding: 3px; 353 | margin: 5px; 354 | background-color: darkred; 355 | color: white; 356 | border-radius: 5px; 357 | } 358 | .expense-management .section { 359 | clear: left; 360 | margin-bottom: 10px; 361 | } 362 | .expense-management .section .title { 363 | font-size: 20px; 364 | } 365 | .expense-description { 366 | width: 100%; 367 | height: 150px; 368 | } 369 | .no-float { 370 | clear: left; 371 | } 372 | .in-line { 373 | display: inline-block; 374 | } 375 | .money { 376 | width: 100px; 377 | font-size: 20px; 378 | } 379 | .percent { 380 | width: 100px; 381 | font-size: 20px; 382 | } 383 | .card-selected { 384 | background-color: orangered; 385 | color: white; 386 | } 387 | .table { 388 | width: 100%; 389 | } 390 | 391 | .loading { 392 | height: 20px; 393 | width: 20px; 394 | float: right; 395 | } 396 | 397 | .more-actions { 398 | margin-top: 30px; 399 | } 400 | .sku-search { 401 | font-size: 30px; 402 | color: white; 403 | border: 0px; 404 | width: 100%; 405 | background-color: #000000; 406 | opacity: 0.1; 407 | text-transform: lowercase; 408 | } 409 | .product-search { 410 | font-size: 30px; 411 | color: white; 412 | border: 0px; 413 | width: 100%; 414 | background-color: #000000; 415 | opacity: 0.1; 416 | text-transform: lowercase; 417 | } 418 | .pay-for-order { 419 | width: 400px; 420 | background-color: #eeeeee; 421 | border: #bbbbbb solid 1px; 422 | padding: 5px; 423 | margin: 20px auto; 424 | } 425 | 426 | .centered { 427 | position: fixed; 428 | top: 50%; 429 | left: 50%; 430 | /* bring your own prefixes */ 431 | transform: translate(-50%, -50%); 432 | } 433 | 434 | .h-centered { 435 | margin: 0 auto; 436 | } 437 | 438 | .payment-method-selected { 439 | background-color: darkorange; 440 | } 441 | -------------------------------------------------------------------------------- /src/components/orderTaking/orderScreen.re: -------------------------------------------------------------------------------- 1 | open Js.Promise; 2 | 3 | open OrderHelper; 4 | 5 | type viewing = 6 | | Tags 7 | | Products(string); 8 | 9 | type modifying = 10 | | Nothing 11 | | CustomerName; 12 | 13 | type state = { 14 | allProducts: list(Product.t), 15 | id: option(string), 16 | customerName: string, 17 | orderItems: list(OrderItem.t), 18 | discounts: list(Discount.t), 19 | createdOn: float, 20 | lastUpdated: option(float), 21 | returned: option(Return.t), 22 | paid: option(Paid.t), 23 | tags: list(string), 24 | viewing, 25 | meta: option(Js.Json.t), 26 | removed: bool, 27 | closedOrder: bool, 28 | modifying, 29 | allDiscounts: list(Discount.t), 30 | sku: string, 31 | showDialog: bool, 32 | skuEnabled: bool, 33 | }; 34 | 35 | type action = 36 | | SelectTag(string) 37 | | SelectProduct(Product.t) 38 | | DeselectTag 39 | | LoadOrder(Order.orderVm) 40 | | CloseOrderScreen 41 | | ChangePaidDate(float) 42 | | ChangeCustomerName(string) 43 | | ChangeOrderItems(list(OrderItem.t)) 44 | | ProductsLoaded(list(Product.t)) 45 | | DiscountsLoaded(list(Discount.t)) 46 | | ApplyDiscount(Discount.t) 47 | | RemoveDiscount(Discount.t) 48 | | ShowDialog 49 | | HideDialog 50 | | EnableSku 51 | | DisableSku; 52 | 53 | let dbUrl = "http://localhost:5984/orders"; 54 | 55 | let component = ReasonReact.reducerComponent("Order"); 56 | 57 | let buildOrder = state : Order.orderVm => { 58 | let order: Order.orderVm = { 59 | id: state.id, 60 | customerName: state.customerName, 61 | orderItems: state.orderItems, 62 | createdOn: state.createdOn, 63 | discounts: state.discounts, 64 | paid: state.paid, 65 | returned: state.returned, 66 | lastUpdated: state.lastUpdated, 67 | removed: state.removed, 68 | meta: state.meta, 69 | }; 70 | /* order |> Order.fromVm |> WebhookEngine.fireForOrder(OrderStarted) |> ignore; */ 71 | order; 72 | }; 73 | 74 | let make = (~goHome, ~goToOrders, _children) => { 75 | ...component, 76 | reducer: (action, state) => 77 | switch (action) { 78 | | ProductsLoaded(products) => 79 | let tags = Product.getTags(products); 80 | ReasonReact.Update({...state, tags, allProducts: products}); 81 | | DiscountsLoaded(discounts) => 82 | ReasonReact.Update({...state, allDiscounts: discounts}) 83 | | ChangeOrderItems(orderItems) => 84 | ReasonReact.UpdateWithSideEffects( 85 | {...state, orderItems}, 86 | (_ => Js.log(state.orderItems)), 87 | ) 88 | | LoadOrder(order) => 89 | Js.log(order); 90 | ReasonReact.Update({ 91 | ...state, 92 | orderItems: order.orderItems, 93 | discounts: order.discounts, 94 | customerName: order.customerName, 95 | paid: order.paid, 96 | returned: order.returned, 97 | id: order.id, 98 | removed: order.removed, 99 | lastUpdated: order.lastUpdated, 100 | createdOn: order.createdOn, 101 | meta: order.meta, 102 | closedOrder: 103 | switch (order.paid) { 104 | | Some(_) => true 105 | | None => false 106 | }, 107 | }); 108 | | ApplyDiscount(dis) => 109 | ReasonReact.Update({ 110 | ...state, 111 | discounts: List.concat([state.discounts, [dis]]), 112 | allDiscounts: 113 | state.allDiscounts 114 | |> List.filter((d: Discount.t) => d.id !== dis.id), 115 | }) 116 | | RemoveDiscount(dis) => 117 | ReasonReact.Update({ 118 | ...state, 119 | discounts: 120 | state.discounts |> List.filter((d: Discount.t) => d.id !== dis.id), 121 | allDiscounts: List.concat([state.allDiscounts, [dis]]), 122 | }) 123 | | ChangeCustomerName(name) => 124 | ReasonReact.Update({...state, customerName: name}) 125 | | ChangePaidDate(date) => 126 | ReasonReact.Update({ 127 | ...state, 128 | paid: 129 | switch (state.paid) { 130 | | None => None 131 | | Some(paid) => Some({...paid, on: date}) 132 | }, 133 | }) 134 | | SelectTag(tag) => 135 | ReasonReact.Update({...state, viewing: Products(tag)}) 136 | | DeselectTag => ReasonReact.Update({...state, viewing: Tags}) 137 | | CloseOrderScreen => 138 | ReasonReact.SideEffects( 139 | ( 140 | _self => { 141 | let go = state.closedOrder ? goToOrders : goHome; 142 | go(); 143 | } 144 | ), 145 | ) 146 | | SelectProduct(product) => 147 | ReasonReact.UpdateWithSideEffects( 148 | { 149 | ...state, 150 | orderItems: 151 | List.concat([ 152 | state.orderItems, 153 | [ 154 | buildOrderItem( 155 | product, 156 | (Date.now() |> string_of_float) 157 | ++ (state.orderItems |> List.length |> string_of_int), 158 | ), 159 | ], 160 | ]), 161 | }, 162 | (self => self.send(HideDialog)), 163 | ) 164 | | ShowDialog => 165 | ReasonReact.UpdateWithSideEffects( 166 | {...state, showDialog: true}, 167 | (self => self.send(DisableSku)), 168 | ) 169 | | HideDialog => 170 | ReasonReact.UpdateWithSideEffects( 171 | {...state, showDialog: false}, 172 | (self => self.send(EnableSku)), 173 | ) 174 | | EnableSku => ReasonReact.Update({...state, skuEnabled: true}) 175 | | DisableSku => ReasonReact.Update({...state, skuEnabled: false}) 176 | }, 177 | initialState: () => { 178 | let queryString = ReasonReact.Router.dangerouslyGetInitialUrl().search; 179 | let customerName = 180 | switch (Util.QueryParam.get("customerName", queryString)) { 181 | | Some(name) => name |> Js.Global.decodeURIComponent 182 | | None => "order.defaultCustomerName" |> Lang.translate 183 | }; 184 | { 185 | orderItems: [], 186 | discounts: [], 187 | meta: None, 188 | removed: false, 189 | createdOn: 0.0, 190 | lastUpdated: None, 191 | id: None, 192 | paid: None, 193 | customerName, 194 | returned: None, 195 | closedOrder: false, 196 | allProducts: [], 197 | tags: [], 198 | viewing: Tags, 199 | modifying: Nothing, 200 | allDiscounts: [], 201 | sku: "", 202 | showDialog: false, 203 | skuEnabled: true, 204 | }; 205 | }, 206 | didMount: self => { 207 | DiscountStore.getAll() 208 | |> then_(discounts => { 209 | self.send(DiscountsLoaded(discounts)); 210 | resolve(); 211 | }) 212 | |> ignore; 213 | ProductStore.getAll() 214 | |> then_(prods => { 215 | self.send(ProductsLoaded(prods)); 216 | resolve(); 217 | }) 218 | |> ignore; 219 | let queryString = ReasonReact.Router.dangerouslyGetInitialUrl().search; 220 | switch (Util.QueryParam.get("orderId", queryString)) { 221 | | None => () 222 | | Some(orderId) => 223 | getOrderVm(orderId) 224 | |> Js.Promise.then_(vm => { 225 | self.send(LoadOrder(vm)); 226 | Js.Promise.resolve(); 227 | }) 228 | |> ignore; 229 | (); 230 | }; 231 | }, 232 | render: self => { 233 | Js.log("orderscreen:: " ++ self.state.customerName); 234 | let deselectTag = _event => self.send(DeselectTag); 235 | let selectTag = tag => self.send(SelectTag(tag)); 236 | let selectProduct = product => self.send(SelectProduct(product)); 237 | let discountSelected = discount => self.send(ApplyDiscount(discount)); 238 | let discountDeselected = discount => self.send(RemoveDiscount(discount)); 239 |
240 | self.send(SelectProduct(p))) 245 | onCancel=(_ => self.send(HideDialog)) 246 | /> 247 |
248 | self.send(CloseOrderScreen)) 251 | onShowProductModal=(_ => self.send(ShowDialog)) 252 | /> 253 |
254 | self.send(ChangeCustomerName(newName))) 258 | /> 259 |
260 |
261 |
262 | ( 263 | if (self.state.closedOrder) { 264 | ; 269 | } else { 270 |
271 | self.send(SelectProduct(p))) 275 | /> 276 | self.send(ChangeOrderItems(orderItems)) 282 | ) 283 | /> 284 |
; 285 | } 286 | ) 287 |
288 |
289 | ( 290 | if (self.state.closedOrder) { 291 | self.send(ChangePaidDate(newDate))) 294 | />; 295 | } else { 296 |
297 | ( 298 | switch (self.state.viewing) { 299 | | Tags => 300 |
301 | ( 302 | self.state.tags 303 | |> List.map(tag => ) 304 | |> Array.of_list 305 | |> ReasonReact.array 306 | ) 307 |
308 | | Products(tag) => 309 |
310 |
325 | } 326 | ) 327 |
328 | 332 |
333 |
334 |
349 |
; 350 | } 351 | ) 352 |
353 |
; 354 | }, 355 | }; --------------------------------------------------------------------------------