├── src ├── assets │ ├── styles.scss │ └── logo.png ├── directives │ └── ClickOutside.js ├── router.js ├── main.js ├── icons │ ├── EmptyState.vue │ ├── Empty4.vue │ ├── Empty3.vue │ ├── Empty2.vue │ └── Empty1.vue ├── components │ ├── Dropdown.vue │ ├── OrderStepper.vue │ ├── Modal.vue │ ├── Notifications │ │ ├── Notifications.vue │ │ └── Notification.vue │ ├── OrderDetail.vue │ ├── HelloWorld.vue │ ├── OrderMap.vue │ └── FilteredOrders.vue ├── App.vue ├── views │ ├── OrderReview.vue │ └── Home.vue └── store.js ├── .env ├── babel.config.js ├── tests └── unit │ ├── .eslintrc.js │ ├── Home.spec.js │ └── FilteredOrders.spec.js ├── public ├── favicon.ico └── index.html ├── postcss.config.js ├── screenshots ├── OrderDetail.png ├── FilteredOrders.png ├── Notifications.png ├── Performance_Data.png └── OrderDetail_DropDown.png ├── .gitignore ├── api ├── index.js └── challenge_data.json ├── package.json └── README.md /src/assets/styles.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VUE_APP_MAPBOX_ACCESS_TOKEN=YOUR_ACCESS_TOKEN -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eros1006/restaurant-management/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eros1006/restaurant-management/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("tailwindcss"), require("autoprefixer")] 3 | }; 4 | -------------------------------------------------------------------------------- /screenshots/OrderDetail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eros1006/restaurant-management/HEAD/screenshots/OrderDetail.png -------------------------------------------------------------------------------- /screenshots/FilteredOrders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eros1006/restaurant-management/HEAD/screenshots/FilteredOrders.png -------------------------------------------------------------------------------- /screenshots/Notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eros1006/restaurant-management/HEAD/screenshots/Notifications.png -------------------------------------------------------------------------------- /screenshots/Performance_Data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eros1006/restaurant-management/HEAD/screenshots/Performance_Data.png -------------------------------------------------------------------------------- /screenshots/OrderDetail_DropDown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eros1006/restaurant-management/HEAD/screenshots/OrderDetail_DropDown.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | /tests/e2e/videos/ 6 | /tests/e2e/screenshots/ 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/directives/ClickOutside.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bind(el, binding, vnode) { 3 | el.event = event => { 4 | if (!(el === event.target || el.contains(event.target))) { 5 | vnode.context[binding.expression](event); 6 | } 7 | }; 8 | document.body.addEventListener("click", el.event); 9 | }, 10 | unbind(el) { 11 | document.body.removeEventListener("click", el.event); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Router from "vue-router"; 3 | import Home from "./views/Home.vue"; 4 | import OrderReview from "./views/OrderReview.vue"; 5 | 6 | Vue.use(Router); 7 | 8 | export default new Router({ 9 | mode: "history", 10 | base: process.env.BASE_URL, 11 | routes: [ 12 | { 13 | path: "/", 14 | component: Home 15 | }, 16 | { 17 | path: "/orders/:id", 18 | component: OrderReview, 19 | props: true 20 | } 21 | // { 22 | // path: "/about", 23 | // name: "about", 24 | // // route level code-splitting 25 | // // this generates a separate chunk (about.[hash].js) for this route 26 | // // which is lazy-loaded when the route is visited. 27 | // component: () => 28 | // import(/* webpackChunkName: "about" */ "./views/About.vue") 29 | // } 30 | ] 31 | }); 32 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | import PortalVue from "portal-vue"; 6 | 7 | // setup socketio connection and vuesocketio integration 8 | import SocketIO from "socket.io-client"; 9 | import VueSocketIO from "vue-socket.io"; 10 | 11 | // custom directive 12 | import ClickOutside from "./directives/ClickOutside"; 13 | Vue.directive("click-outside", ClickOutside); 14 | 15 | Vue.use( 16 | new VueSocketIO({ 17 | // debug: true, 18 | connection: SocketIO("http://localhost:3000"), 19 | vuex: { 20 | store, 21 | actionPrefix: "SOCKET_", 22 | mutationPrefix: "SOCKET_" 23 | } 24 | }) 25 | ); 26 | 27 | Vue.config.productionTip = false; 28 | 29 | Vue.use(PortalVue); 30 | 31 | new Vue({ 32 | router, 33 | store, 34 | render: h => h(App) 35 | }).$mount("#app"); 36 | -------------------------------------------------------------------------------- /src/icons/EmptyState.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/unit/Home.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount, createLocalVue } from "@vue/test-utils"; 2 | import Home from "@/views/Home.vue"; 3 | import Vuex from "vuex"; 4 | 5 | const localVue = createLocalVue(); 6 | localVue.use(Vuex); 7 | 8 | describe("Home.vue", () => { 9 | let getters; 10 | let store; 11 | 12 | beforeEach(() => { 13 | getters = { 14 | orders() { 15 | return []; 16 | }, 17 | getOrdersByFilter() { 18 | return () => { 19 | return []; 20 | }; 21 | }, 22 | deliveredOrders() { 23 | return []; 24 | } 25 | }; 26 | 27 | store = new Vuex.Store({ 28 | state: {}, 29 | getters 30 | }); 31 | }); 32 | 33 | it("renders title", () => { 34 | const wrapper = shallowMount(Home, { store, localVue }); 35 | const title = wrapper.find("h1"); 36 | expect(title.text()).toMatch("Restaurant Overview"); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/unit/FilteredOrders.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount, createLocalVue } from "@vue/test-utils"; 2 | import FilteredOrders from "@/components/FilteredOrders.vue"; 3 | import DropDown from "@/components/DropDown.vue"; 4 | import Vuex from "vuex"; 5 | 6 | const localVue = createLocalVue(); 7 | localVue.use(Vuex); 8 | 9 | describe("FilteredOrders.vue", () => { 10 | let getters; 11 | let store; 12 | 13 | beforeEach(() => { 14 | getters = { 15 | orders() { 16 | return []; 17 | }, 18 | getOrdersByFilter() { 19 | return () => { 20 | return []; 21 | }; 22 | } 23 | }; 24 | 25 | store = new Vuex.Store({ 26 | state: {}, 27 | getters 28 | }); 29 | }); 30 | 31 | it("renders title", () => { 32 | const wrapper = shallowMount(FilteredOrders, { store, localVue }); 33 | const title = wrapper.find("h1"); 34 | expect(title.text()).toMatch("Orders"); 35 | }); 36 | 37 | it("renders Dropdown when user clicks on filter options", () => { 38 | const wrapper = shallowMount(FilteredOrders, { store, localVue }); 39 | wrapper.find("#current-status").trigger("click"); 40 | expect(wrapper.findAll(DropDown).length).toEqual(1); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Restaurant Management 19 | 20 | 21 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/Dropdown.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 44 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | 24 | 63 | -------------------------------------------------------------------------------- /src/views/OrderReview.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | const app = require("express")(); 2 | const http = require("http").Server(app); 3 | const io = require("socket.io")(http); 4 | 5 | const fs = require("fs"); 6 | const events = JSON.parse(fs.readFileSync("./challenge_data.json", "utf8")); 7 | 8 | // const sortedEvents = events.sort((a, b) => a.sent_at_second - b.sent_at_second); 9 | 10 | // function sleep(duration) { 11 | // return new Promise(resolve => setTimeout(resolve, duration)); 12 | // } 13 | 14 | io.on("connection", async function(socket) { 15 | console.log("a user connected"); 16 | 17 | socket.on("disconnect", function() { 18 | console.log("user disconnected"); 19 | }); 20 | 21 | // let ticker = 0; 22 | // while (sortedEvents.length) { 23 | // for (let i = 0; i < sortedEvents.length; i++) { 24 | // if (sortedEvents[i].sent_at_second === ticker) { 25 | // io.emit("new_event", JSON.stringify(sortedEvents[i])); 26 | // sortedEvents.splice(0, 1); 27 | // } else { 28 | // break; 29 | // } 30 | // } 31 | 32 | // await sleep(1000); 33 | // ticker++; 34 | // } 35 | 36 | let currentIndex = 0; 37 | 38 | const interval = setInterval(() => { 39 | const currentEvents = events.filter( 40 | event => event.sent_at_second === currentIndex 41 | ); 42 | currentEvents.forEach(event => io.emit("new_event", JSON.stringify(event))); 43 | 44 | currentIndex++; 45 | 46 | // clearInterval after all events have been processed 47 | if (currentIndex === events.length) { 48 | clearInterval(interval); 49 | } 50 | }, 1000); 51 | }); 52 | 53 | http.listen(3000, function() { 54 | console.log("listening on *:3000"); 55 | }); 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restaurant-management", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve --mode development", 7 | "api": "cd api && node .", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint", 10 | "test:e2e": "vue-cli-service test:e2e", 11 | "test:unit": "vue-cli-service test:unit", 12 | "simulation": "npm run api & npm run serve" 13 | }, 14 | "dependencies": { 15 | "core-js": "^2.6.5", 16 | "express": "^4.15.2", 17 | "lodash": "^4.17.11", 18 | "mapbox-gl": "^1.0.0", 19 | "portal-vue": "^2.1.5", 20 | "socket.io": "^2.2.0", 21 | "vue": "^2.6.10", 22 | "vue-router": "^3.0.3", 23 | "vue-socket.io": "^3.0.7", 24 | "vuex": "^3.0.1" 25 | }, 26 | "devDependencies": { 27 | "@vue/cli-plugin-babel": "^3.8.0", 28 | "@vue/cli-plugin-e2e-cypress": "^3.8.0", 29 | "@vue/cli-plugin-eslint": "^3.8.0", 30 | "@vue/cli-plugin-unit-jest": "^3.8.0", 31 | "@vue/cli-service": "^3.8.0", 32 | "@vue/eslint-config-prettier": "^4.0.1", 33 | "@vue/test-utils": "1.0.0-beta.29", 34 | "babel-core": "7.0.0-bridge.0", 35 | "babel-eslint": "^10.0.1", 36 | "babel-jest": "^23.0.0", 37 | "eslint": "^5.16.0", 38 | "eslint-plugin-vue": "^5.0.0", 39 | "node-sass": "^4.9.0", 40 | "sass-loader": "^7.1.0", 41 | "tailwindcss": "^1.0.3", 42 | "vue-template-compiler": "^2.6.10" 43 | }, 44 | "eslintConfig": { 45 | "root": true, 46 | "env": { 47 | "node": true 48 | }, 49 | "extends": [ 50 | "plugin:vue/essential", 51 | "@vue/prettier" 52 | ], 53 | "rules": {}, 54 | "parserOptions": { 55 | "parser": "babel-eslint" 56 | } 57 | }, 58 | "browserslist": [ 59 | "> 1%", 60 | "last 2 versions" 61 | ], 62 | "jest": { 63 | "moduleFileExtensions": [ 64 | "js", 65 | "jsx", 66 | "json", 67 | "vue" 68 | ], 69 | "transform": { 70 | "^.+\\.vue$": "vue-jest", 71 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": "jest-transform-stub", 72 | "^.+\\.jsx?$": "babel-jest" 73 | }, 74 | "transformIgnorePatterns": [ 75 | "/node_modules/*" 76 | ], 77 | "moduleNameMapper": { 78 | "^@/(.*)$": "/src/$1" 79 | }, 80 | "snapshotSerializers": [ 81 | "jest-serializer-vue" 82 | ], 83 | "testMatch": [ 84 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" 85 | ], 86 | "testURL": "http://localhost/", 87 | "watchPlugins": [ 88 | "jest-watch-typeahead/filename", 89 | "jest-watch-typeahead/testname" 90 | ] 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/components/OrderStepper.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 71 | 72 | 89 | -------------------------------------------------------------------------------- /src/components/Modal.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 106 | -------------------------------------------------------------------------------- /src/components/Notifications/Notifications.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 91 | 92 | 114 | -------------------------------------------------------------------------------- /src/components/OrderDetail.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 104 | 105 | 113 | 114 | 115 | 131 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import _ from "lodash"; 4 | 5 | Vue.use(Vuex); 6 | 7 | export default new Vuex.Store({ 8 | state: { 9 | isConnected: false, 10 | orders: [], 11 | showAllEvents: false, 12 | filters: [ 13 | { text: "Cooking Now", value: "CREATED" }, 14 | { text: "Cooked", value: "COOKED" }, 15 | { text: "Driver en route", value: "DRIVER_RECEIVED" }, 16 | { text: "Delivered", value: "DELIVERED" }, 17 | { text: "Cancelled", value: "CANCELLED" } 18 | ], 19 | topSellers: {}, 20 | slowItems: {} 21 | }, 22 | mutations: { 23 | SOCKET_connect(state) { 24 | state.isConnected = Date.now(); 25 | 26 | state.orders.push({ 27 | message: "Connected", 28 | subtext: "Simulation started", 29 | id: Date.now(), 30 | showNotification: true 31 | }); 32 | }, 33 | 34 | SOCKET_disconnect(state) { 35 | state.isConnected = false; 36 | }, 37 | 38 | SOCKET_new_event(state, message) { 39 | const newEvent = JSON.parse(message); 40 | const eventsCopy = state.orders.slice(); 41 | let existingEvent = _.find(eventsCopy, { id: newEvent.id }); 42 | 43 | // CREATE NEW EVENT 44 | // only show notifications for new orders 45 | if (newEvent.event_name === "CREATED") { 46 | newEvent.showNotification = true; 47 | } else { 48 | newEvent.showNotification = false; 49 | } 50 | 51 | // UPDATE EXISTING EVENT 52 | if (existingEvent) { 53 | const { event_name } = newEvent; 54 | Vue.set(existingEvent, "currentStatus", event_name); 55 | Vue.set(existingEvent.events, event_name, newEvent); 56 | 57 | Vue.set(state, "orders", eventsCopy); 58 | } else { 59 | // NEW EVENT 60 | eventsCopy.push({ 61 | ...newEvent, 62 | events: {}, // for maximum customer order edit-ability, we'll store all future events in an events property 63 | currentStatus: "CREATED" 64 | }); 65 | 66 | if (eventsCopy[0].events) { 67 | // add the CREATED event into events object as well 68 | eventsCopy[0].events["CREATED"] = newEvent; 69 | } 70 | 71 | Vue.set(state, "orders", eventsCopy); 72 | } 73 | 74 | // collect meta data 75 | const { name } = newEvent; 76 | const words = name.split(" "); 77 | 78 | words.forEach(word => { 79 | word = word.toLowerCase(); 80 | 81 | if (!state.topSellers[word]) { 82 | state.topSellers[word] = 1; 83 | Vue.set(state.topSellers, word, 1); 84 | } else { 85 | const newCount = (state.topSellers[word] += 1); 86 | Vue.set(state.topSellers, word, newCount); 87 | } 88 | }); 89 | }, 90 | 91 | HIDE_NOTIFICATION(state, id) { 92 | const notifications = _.filter(state.orders, { id }); 93 | notifications.forEach(notification => { 94 | notification.showNotification = false; 95 | }); 96 | }, 97 | 98 | UPDATE_ORDER_STATUS(state, { orderId, newStatus }) { 99 | const existingEvent = _.find(state.orders, { id: orderId }); 100 | 101 | Vue.set(existingEvent, "currentStatus", newStatus); 102 | // add event to order events with sent_at_second calculation 103 | Vue.set(existingEvent.events, newStatus, { 104 | event_name: "DELIVERED", 105 | sent_at_second: (state.isConnected - Date.now()) / 1000 106 | }); 107 | }, 108 | 109 | TOGGLE_SHOW_ALL_EVENTS(state) { 110 | state.showAllEvents = !state.showAllEvents; 111 | } 112 | }, 113 | getters: { 114 | orders: ({ orders }) => orders, 115 | getOrdersByFilter: state => filter => { 116 | return state.orders.filter(order => order.currentStatus === filter); 117 | }, 118 | topSellers: ({ topSellers }) => topSellers, 119 | deliveredOrders: ({ orders }) => { 120 | return orders.filter(order => order.currentStatus === "DELIVERED"); 121 | } 122 | } 123 | }); 124 | -------------------------------------------------------------------------------- /src/components/Notifications/Notification.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 116 | 117 | 123 | -------------------------------------------------------------------------------- /src/components/OrderMap.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 121 | 122 | 157 | 158 | 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Restaurant Order Management Web App 2 | Built with VueJS, Vuex, socket-io, and tailwindcss 3 | 4 | # Screenshots 5 | Notifications 6 | ![Notifcations](/screenshots/Notifications.png?raw=true) 7 | 8 | Order Detail 9 | ![Order Detail](/screenshots/OrderDetail.png?raw=true) 10 | 11 | Take manual action on an order 12 | ![Order Detail](/screenshots/OrderDetail_DropDown.png?raw=true) 13 | 14 | Filtered order view 15 | ![Filtered order view](/screenshots/FilteredOrders.png?raw=true) 16 | 17 | Restaurant Performance 18 | Take manual action on an order 19 | ![Restaurant performance](/screenshots/Performance_Data.png?raw=true) 20 | 21 | 22 | ## Technology reasoning 23 | - VueJS for reusable component design paradigm 24 | - Vuex for a global state container (following flux architecture) 25 | - socket-io to communicate real-time orders (vue-socket.io for it's tight integration with vuex) 26 | - tailwindcss to build a custom design system that isn't the same as every other website on the internet (i.e. Bootstrap) 27 | 28 | 29 | # Steps to run project 30 | ## 1. Get Mapbox API key and add to .env file like so 31 | In the .env file, replace `YOUR_ACCESS_TOKEN` with your mapbox access token, which you can create for free at mapbox.com. 32 | 33 | **Order Detail view will not show a map view if you don't have this setup.** 34 | ``` 35 | VUE_APP_MAPBOX_ACCESS_TOKEN=YOUR_ACCESS_TOKEN 36 | ``` 37 | 38 | ## 2. Install Dependencies 39 | ``` 40 | npm install 41 | ``` 42 | 43 | ## 3. Run simulation 44 | ``` 45 | npm run simulation 46 | ``` 47 | 48 | 49 | 50 | # Design Decisions 51 | BE sends the following payload signature for each order: 52 | ``` 53 | { 54 | destination: "801 Toyopa Dr, Pacific Palisades, CA 90272", 55 | event_name: "CREATED", 56 | id: "4b76edbf", 57 | name: "Cheese pizza", 58 | sent_at_second: 4 59 | }, 60 | ``` 61 | 62 | Orders are stored in a global state container, following flux design patterns, specifically using Vuex. I chose to use npm package `vue-socket.io` to listen to socket-io events. I use `vue-socket.io` to listen for `new_event` emitted from socket-io at port 3000. `SOCKET_new_event` mutation processes the event. 63 | 64 | On the FE I store orders like so: 65 | ``` 66 | { 67 | destination: "801 Toyopa Dr, Pacific Palisades, CA 90272", 68 | event_name: "CREATED", 69 | id: "4b76edbf", 70 | name: "Cheese pizza", 71 | sent_at_second: 4, 72 | 73 | currentStatus: String, // new event status, 74 | events: { 75 | EVENT_NAME: { new_event_payload } 76 | } 77 | }, 78 | 79 | ``` 80 | - Because there are only 5 possible `event_name` (`CREATED`, `COOKED`, `DRIVER_RECEIVED`, `DELIVERED`, and `CANCELLED`), I chose to store each new event in an `events {}` (instead of in an array). This gives me instant O(1) access to whichever order event I'm looking for. For example, if I want to know when an event was delivered, in any component I can simply do `order.events.DELIVERED` directly in any component without having to lookup within an array of events. The one downside of this approach is we don't keep a good log of all events in the _order_ in which they are received. However, I do store a `sent_at_second` property when a user manually updates an order, so we *could* stitch together the order history in the correct order if such a request was made by Product & Design :) But at that point I would push for us to add a new api endpoint that fetches order history because we do not need the entire order history in most views. 81 | 82 | ## Views 83 | ### 1. Home 84 | - I built the home page as a dashboard with various modules that a restaurant owner would find most useful. 85 | - Notifications show the user orders (and order updates) as they are being received in real-time. This view can get overwhelming so I added the option to filter to only new orders, and additionally the option to close the notifications side bar. 86 | - The top row of filter options gives you a quick count of each filter and the number of items in that status currently. Clicking on any of these filters updates the orders you see right below. 87 | - I also add 3 sections at the bottom of the dashboard: 88 | - Best sellers: to see which ingredients are most popular 89 | - it's a simply word map count, I thought about pulling in a NLP library to categorize by food type (deserts, meals, and appetizers) or by culture (American, Italian, Middle Eastern) but time did not allow me to pursue this. 90 | - Time to delivery 91 | - a valuable metric for a restaurant owner is how long does it take for the restaurant to deliver items from the item of order. As a business owner he/she can help encourage their team/employees to track this value and improve over time. 92 | - Average order value 93 | - this seems like the most valuable information for the restaurant owner, but due to lack of $$$ data I just left this as a placeholder for future development. 94 | 95 | ### 2. Order Review 96 | - I started the entire project by building a dedicated `/orders/:id` page that can host information about any 1 order and can properly display all the relevant information. This page is routed to by clicking on a notification. 97 | 98 | 99 | 100 | # If I had more time! 101 | - Update BE to send minimal payloads when order status changes, all we need on FE is the `id` and `event_name` on a new status order, or if the order item itself is being changed we could simply send `{ id: ID, changes: { ... } }`. Right now we receive the entire `order` {}. 102 | - Add better animations when order statuses change 103 | - Show full order history (see discussion above) 104 | - Right now I assume most orders will transition between these 5 statuses: CREATED -> COOKED -> DRIVER_RECEIVED -> DELIVERED (with the option of it becoming CANCELLED at any point) 105 | - Show in the dashboard at which point orders are being CANCELLED. If they've already been cooked or are en route, the restaurant is bleeding money, this is valuable information for restaurant owners to know. 106 | - Add more thorough unit and e2e tests, specifically testing each component and various order status changes. 107 | - Explore adding a FE state machine design pattern for each order. 108 | - Use a NLP library to categorize food items by type of cuisine. 109 | 110 | 111 | # Tests 112 | ### Run unit tests 113 | ``` 114 | npm run test:unit 115 | ``` 116 | -------------------------------------------------------------------------------- /src/components/FilteredOrders.vue: -------------------------------------------------------------------------------- 1 | 120 | 121 | 199 | 200 | 205 | -------------------------------------------------------------------------------- /src/icons/Empty4.vue: -------------------------------------------------------------------------------- 1 | 186 | 187 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 201 | 202 | 271 | 272 | 294 | -------------------------------------------------------------------------------- /api/challenge_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 4 | "event_name": "CREATED", 5 | "id": "4b76edbf", 6 | "name": "Cheese pizza", 7 | "sent_at_second": 4 8 | }, 9 | { 10 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 11 | "event_name": "COOKED", 12 | "id": "4b76edbf", 13 | "name": "Cheese pizza", 14 | "sent_at_second": 10 15 | }, 16 | { 17 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 18 | "event_name": "DRIVER_RECEIVED", 19 | "id": "4b76edbf", 20 | "name": "Cheese pizza", 21 | "sent_at_second": 17 22 | }, 23 | { 24 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 25 | "event_name": "DELIVERED", 26 | "id": "4b76edbf", 27 | "name": "Cheese pizza", 28 | "sent_at_second": 23 29 | }, 30 | { 31 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 32 | "event_name": "CREATED", 33 | "id": "f7711c3b", 34 | "name": "Mushroom pizza", 35 | "sent_at_second": 3 36 | }, 37 | { 38 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 39 | "event_name": "COOKED", 40 | "id": "f7711c3b", 41 | "name": "Mushroom pizza", 42 | "sent_at_second": 6 43 | }, 44 | { 45 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 46 | "event_name": "CANCELLED", 47 | "id": "f7711c3b", 48 | "name": "Mushroom pizza", 49 | "sent_at_second": 9 50 | }, 51 | { 52 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 53 | "event_name": "CREATED", 54 | "id": "79ff5fa7", 55 | "name": "Pepperoni pizza", 56 | "sent_at_second": 7 57 | }, 58 | { 59 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 60 | "event_name": "COOKED", 61 | "id": "79ff5fa7", 62 | "name": "Pepperoni pizza", 63 | "sent_at_second": 12 64 | }, 65 | { 66 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 67 | "event_name": "DRIVER_RECEIVED", 68 | "id": "79ff5fa7", 69 | "name": "Pepperoni pizza", 70 | "sent_at_second": 18 71 | }, 72 | { 73 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 74 | "event_name": "DELIVERED", 75 | "id": "79ff5fa7", 76 | "name": "Pepperoni pizza", 77 | "sent_at_second": 21 78 | }, 79 | { 80 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 81 | "event_name": "CREATED", 82 | "id": "2d181b1a", 83 | "name": "Veggie pizza", 84 | "sent_at_second": 9 85 | }, 86 | { 87 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 88 | "event_name": "COOKED", 89 | "id": "2d181b1a", 90 | "name": "Veggie pizza", 91 | "sent_at_second": 15 92 | }, 93 | { 94 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 95 | "event_name": "DRIVER_RECEIVED", 96 | "id": "2d181b1a", 97 | "name": "Veggie pizza", 98 | "sent_at_second": 20 99 | }, 100 | { 101 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 102 | "event_name": "DELIVERED", 103 | "id": "2d181b1a", 104 | "name": "Veggie pizza", 105 | "sent_at_second": 29 106 | }, 107 | { 108 | "destination": "14622 Demblon St, Baldwin Park, CA 91706", 109 | "event_name": "CREATED", 110 | "id": "1c891522", 111 | "name": "Vanilla ice cream", 112 | "sent_at_second": 13 113 | }, 114 | { 115 | "destination": "14622 Demblon St, Baldwin Park, CA 91706", 116 | "event_name": "COOKED", 117 | "id": "1c891522", 118 | "name": "Vanilla ice cream", 119 | "sent_at_second": 21 120 | }, 121 | { 122 | "destination": "14622 Demblon St, Baldwin Park, CA 91706", 123 | "event_name": "CANCELLED", 124 | "id": "1c891522", 125 | "name": "Vanilla ice cream", 126 | "sent_at_second": 27 127 | }, 128 | { 129 | "destination": "1041 S Fairfax Ave, Los Angeles, CA 90019", 130 | "event_name": "CREATED", 131 | "id": "031c71b2", 132 | "name": "Chocolate ice cream", 133 | "sent_at_second": 9 134 | }, 135 | { 136 | "destination": "1041 S Fairfax Ave, Los Angeles, CA 90019", 137 | "event_name": "COOKED", 138 | "id": "031c71b2", 139 | "name": "Chocolate ice cream", 140 | "sent_at_second": 16 141 | }, 142 | { 143 | "destination": "1041 S Fairfax Ave, Los Angeles, CA 90019", 144 | "event_name": "CANCELLED", 145 | "id": "031c71b2", 146 | "name": "Chocolate ice cream", 147 | "sent_at_second": 22 148 | }, 149 | { 150 | "destination": "14622 Demblon St, Baldwin Park, CA 91706", 151 | "event_name": "CREATED", 152 | "id": "ff5bd568", 153 | "name": "Mint ice cream", 154 | "sent_at_second": 10 155 | }, 156 | { 157 | "destination": "14622 Demblon St, Baldwin Park, CA 91706", 158 | "event_name": "COOKED", 159 | "id": "ff5bd568", 160 | "name": "Mint ice cream", 161 | "sent_at_second": 15 162 | }, 163 | { 164 | "destination": "14622 Demblon St, Baldwin Park, CA 91706", 165 | "event_name": "DRIVER_RECEIVED", 166 | "id": "ff5bd568", 167 | "name": "Mint ice cream", 168 | "sent_at_second": 19 169 | }, 170 | { 171 | "destination": "14622 Demblon St, Baldwin Park, CA 91706", 172 | "event_name": "DELIVERED", 173 | "id": "ff5bd568", 174 | "name": "Mint ice cream", 175 | "sent_at_second": 22 176 | }, 177 | { 178 | "destination": "704 24th St, Santa Monica, CA 90402", 179 | "event_name": "CREATED", 180 | "id": "8a06f9de", 181 | "name": "Noodles", 182 | "sent_at_second": 11 183 | }, 184 | { 185 | "destination": "704 24th St, Santa Monica, CA 90402", 186 | "event_name": "COOKED", 187 | "id": "8a06f9de", 188 | "name": "Noodles", 189 | "sent_at_second": 17 190 | }, 191 | { 192 | "destination": "704 24th St, Santa Monica, CA 90402", 193 | "event_name": "CANCELLED", 194 | "id": "8a06f9de", 195 | "name": "Noodles", 196 | "sent_at_second": 21 197 | }, 198 | { 199 | "destination": "139 E 66th St, Los Angeles, CA 90003", 200 | "event_name": "CREATED", 201 | "id": "43bf7f03", 202 | "name": "Cheese burger", 203 | "sent_at_second": 13 204 | }, 205 | { 206 | "destination": "139 E 66th St, Los Angeles, CA 90003", 207 | "event_name": "COOKED", 208 | "id": "43bf7f03", 209 | "name": "Cheese burger", 210 | "sent_at_second": 21 211 | }, 212 | { 213 | "destination": "139 E 66th St, Los Angeles, CA 90003", 214 | "event_name": "DRIVER_RECEIVED", 215 | "id": "43bf7f03", 216 | "name": "Cheese burger", 217 | "sent_at_second": 30 218 | }, 219 | { 220 | "destination": "139 E 66th St, Los Angeles, CA 90003", 221 | "event_name": "DELIVERED", 222 | "id": "43bf7f03", 223 | "name": "Cheese burger", 224 | "sent_at_second": 40 225 | }, 226 | { 227 | "destination": "7697 Everest Pl, Rancho Cucamonga, CA 91730", 228 | "event_name": "CREATED", 229 | "id": "e765bcfd", 230 | "name": "Vegan burger", 231 | "sent_at_second": 21 232 | }, 233 | { 234 | "destination": "7697 Everest Pl, Rancho Cucamonga, CA 91730", 235 | "event_name": "COOKED", 236 | "id": "e765bcfd", 237 | "name": "Vegan burger", 238 | "sent_at_second": 27 239 | }, 240 | { 241 | "destination": "7697 Everest Pl, Rancho Cucamonga, CA 91730", 242 | "event_name": "DRIVER_RECEIVED", 243 | "id": "e765bcfd", 244 | "name": "Vegan burger", 245 | "sent_at_second": 35 246 | }, 247 | { 248 | "destination": "7697 Everest Pl, Rancho Cucamonga, CA 91730", 249 | "event_name": "DELIVERED", 250 | "id": "e765bcfd", 251 | "name": "Vegan burger", 252 | "sent_at_second": 41 253 | }, 254 | { 255 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 256 | "event_name": "CREATED", 257 | "id": "ba133c49", 258 | "name": "Chicken sandwich", 259 | "sent_at_second": 21 260 | }, 261 | { 262 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 263 | "event_name": "COOKED", 264 | "id": "ba133c49", 265 | "name": "Chicken sandwich", 266 | "sent_at_second": 27 267 | }, 268 | { 269 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 270 | "event_name": "DRIVER_RECEIVED", 271 | "id": "ba133c49", 272 | "name": "Chicken sandwich", 273 | "sent_at_second": 35 274 | }, 275 | { 276 | "destination": "801 Toyopa Dr, Pacific Palisades, CA 90272", 277 | "event_name": "DELIVERED", 278 | "id": "ba133c49", 279 | "name": "Chicken sandwich", 280 | "sent_at_second": 39 281 | }, 282 | { 283 | "destination": "139 E 66th St, Los Angeles, CA 90003", 284 | "event_name": "CREATED", 285 | "id": "0b7d8f57", 286 | "name": "Roast beef sandwich", 287 | "sent_at_second": 16 288 | }, 289 | { 290 | "destination": "139 E 66th St, Los Angeles, CA 90003", 291 | "event_name": "COOKED", 292 | "id": "0b7d8f57", 293 | "name": "Roast beef sandwich", 294 | "sent_at_second": 26 295 | }, 296 | { 297 | "destination": "139 E 66th St, Los Angeles, CA 90003", 298 | "event_name": "DRIVER_RECEIVED", 299 | "id": "0b7d8f57", 300 | "name": "Roast beef sandwich", 301 | "sent_at_second": 30 302 | }, 303 | { 304 | "destination": "139 E 66th St, Los Angeles, CA 90003", 305 | "event_name": "DELIVERED", 306 | "id": "0b7d8f57", 307 | "name": "Roast beef sandwich", 308 | "sent_at_second": 37 309 | }, 310 | { 311 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 312 | "event_name": "CREATED", 313 | "id": "54171577", 314 | "name": "Vegan sandwich", 315 | "sent_at_second": 26 316 | }, 317 | { 318 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 319 | "event_name": "COOKED", 320 | "id": "54171577", 321 | "name": "Vegan sandwich", 322 | "sent_at_second": 30 323 | }, 324 | { 325 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 326 | "event_name": "DRIVER_RECEIVED", 327 | "id": "54171577", 328 | "name": "Vegan sandwich", 329 | "sent_at_second": 40 330 | }, 331 | { 332 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 333 | "event_name": "DELIVERED", 334 | "id": "54171577", 335 | "name": "Vegan sandwich", 336 | "sent_at_second": 47 337 | }, 338 | { 339 | "destination": "808 E Pine Ave, El Segundo, CA 90245", 340 | "event_name": "CREATED", 341 | "id": "7efe7226", 342 | "name": "Casesar salad", 343 | "sent_at_second": 23 344 | }, 345 | { 346 | "destination": "808 E Pine Ave, El Segundo, CA 90245", 347 | "event_name": "COOKED", 348 | "id": "7efe7226", 349 | "name": "Casesar salad", 350 | "sent_at_second": 28 351 | }, 352 | { 353 | "destination": "808 E Pine Ave, El Segundo, CA 90245", 354 | "event_name": "DRIVER_RECEIVED", 355 | "id": "7efe7226", 356 | "name": "Casesar salad", 357 | "sent_at_second": 31 358 | }, 359 | { 360 | "destination": "808 E Pine Ave, El Segundo, CA 90245", 361 | "event_name": "DELIVERED", 362 | "id": "7efe7226", 363 | "name": "Casesar salad", 364 | "sent_at_second": 40 365 | }, 366 | { 367 | "destination": "2094 Cedar Ave, Long Beach, CA 90806", 368 | "event_name": "CREATED", 369 | "id": "b99810cd", 370 | "name": "Vegetarian lasagna", 371 | "sent_at_second": 28 372 | }, 373 | { 374 | "destination": "2094 Cedar Ave, Long Beach, CA 90806", 375 | "event_name": "COOKED", 376 | "id": "b99810cd", 377 | "name": "Vegetarian lasagna", 378 | "sent_at_second": 36 379 | }, 380 | { 381 | "destination": "2094 Cedar Ave, Long Beach, CA 90806", 382 | "event_name": "DRIVER_RECEIVED", 383 | "id": "b99810cd", 384 | "name": "Vegetarian lasagna", 385 | "sent_at_second": 41 386 | }, 387 | { 388 | "destination": "2094 Cedar Ave, Long Beach, CA 90806", 389 | "event_name": "DELIVERED", 390 | "id": "b99810cd", 391 | "name": "Vegetarian lasagna", 392 | "sent_at_second": 44 393 | }, 394 | { 395 | "destination": "147 N Hamel Dr, Beverly Hills, CA 90211", 396 | "event_name": "CREATED", 397 | "id": "c743e5e3", 398 | "name": "Meat lasagna", 399 | "sent_at_second": 28 400 | }, 401 | { 402 | "destination": "147 N Hamel Dr, Beverly Hills, CA 90211", 403 | "event_name": "COOKED", 404 | "id": "c743e5e3", 405 | "name": "Meat lasagna", 406 | "sent_at_second": 34 407 | }, 408 | { 409 | "destination": "147 N Hamel Dr, Beverly Hills, CA 90211", 410 | "event_name": "DRIVER_RECEIVED", 411 | "id": "c743e5e3", 412 | "name": "Meat lasagna", 413 | "sent_at_second": 40 414 | }, 415 | { 416 | "destination": "147 N Hamel Dr, Beverly Hills, CA 90211", 417 | "event_name": "DELIVERED", 418 | "id": "c743e5e3", 419 | "name": "Meat lasagna", 420 | "sent_at_second": 46 421 | }, 422 | { 423 | "destination": "1296 N Berkeley Ave, San Bernardino, CA 92405", 424 | "event_name": "CREATED", 425 | "id": "a801b621", 426 | "name": "Eggplant parmesan", 427 | "sent_at_second": 26 428 | }, 429 | { 430 | "destination": "1296 N Berkeley Ave, San Bernardino, CA 92405", 431 | "event_name": "COOKED", 432 | "id": "a801b621", 433 | "name": "Eggplant parmesan", 434 | "sent_at_second": 36 435 | }, 436 | { 437 | "destination": "1296 N Berkeley Ave, San Bernardino, CA 92405", 438 | "event_name": "CANCELLED", 439 | "id": "a801b621", 440 | "name": "Eggplant parmesan", 441 | "sent_at_second": 45 442 | }, 443 | { 444 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 445 | "event_name": "CREATED", 446 | "id": "e378261d", 447 | "name": "Spaghetti", 448 | "sent_at_second": 35 449 | }, 450 | { 451 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 452 | "event_name": "COOKED", 453 | "id": "e378261d", 454 | "name": "Spaghetti", 455 | "sent_at_second": 39 456 | }, 457 | { 458 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 459 | "event_name": "DRIVER_RECEIVED", 460 | "id": "e378261d", 461 | "name": "Spaghetti", 462 | "sent_at_second": 48 463 | }, 464 | { 465 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 466 | "event_name": "DELIVERED", 467 | "id": "e378261d", 468 | "name": "Spaghetti", 469 | "sent_at_second": 55 470 | }, 471 | { 472 | "destination": "126 N Arden Blvd, Los Angeles, CA 90004", 473 | "event_name": "CREATED", 474 | "id": "040717a0", 475 | "name": "Pad thai", 476 | "sent_at_second": 34 477 | }, 478 | { 479 | "destination": "126 N Arden Blvd, Los Angeles, CA 90004", 480 | "event_name": "COOKED", 481 | "id": "040717a0", 482 | "name": "Pad thai", 483 | "sent_at_second": 42 484 | }, 485 | { 486 | "destination": "126 N Arden Blvd, Los Angeles, CA 90004", 487 | "event_name": "DRIVER_RECEIVED", 488 | "id": "040717a0", 489 | "name": "Pad thai", 490 | "sent_at_second": 50 491 | }, 492 | { 493 | "destination": "126 N Arden Blvd, Los Angeles, CA 90004", 494 | "event_name": "DELIVERED", 495 | "id": "040717a0", 496 | "name": "Pad thai", 497 | "sent_at_second": 53 498 | }, 499 | { 500 | "destination": "1296 N Berkeley Ave, San Bernardino, CA 92405", 501 | "event_name": "CREATED", 502 | "id": "f1278cf2", 503 | "name": "Orange chicken", 504 | "sent_at_second": 34 505 | }, 506 | { 507 | "destination": "1296 N Berkeley Ave, San Bernardino, CA 92405", 508 | "event_name": "COOKED", 509 | "id": "f1278cf2", 510 | "name": "Orange chicken", 511 | "sent_at_second": 40 512 | }, 513 | { 514 | "destination": "1296 N Berkeley Ave, San Bernardino, CA 92405", 515 | "event_name": "DRIVER_RECEIVED", 516 | "id": "f1278cf2", 517 | "name": "Orange chicken", 518 | "sent_at_second": 49 519 | }, 520 | { 521 | "destination": "1296 N Berkeley Ave, San Bernardino, CA 92405", 522 | "event_name": "DELIVERED", 523 | "id": "f1278cf2", 524 | "name": "Orange chicken", 525 | "sent_at_second": 56 526 | }, 527 | { 528 | "destination": "2094 Cedar Ave, Long Beach, CA 90806", 529 | "event_name": "CREATED", 530 | "id": "7dda55b7", 531 | "name": "Fried rice", 532 | "sent_at_second": 33 533 | }, 534 | { 535 | "destination": "2094 Cedar Ave, Long Beach, CA 90806", 536 | "event_name": "COOKED", 537 | "id": "7dda55b7", 538 | "name": "Fried rice", 539 | "sent_at_second": 40 540 | }, 541 | { 542 | "destination": "2094 Cedar Ave, Long Beach, CA 90806", 543 | "event_name": "DRIVER_RECEIVED", 544 | "id": "7dda55b7", 545 | "name": "Fried rice", 546 | "sent_at_second": 46 547 | }, 548 | { 549 | "destination": "2094 Cedar Ave, Long Beach, CA 90806", 550 | "event_name": "DELIVERED", 551 | "id": "7dda55b7", 552 | "name": "Fried rice", 553 | "sent_at_second": 54 554 | }, 555 | { 556 | "destination": "510 18th St, Santa Monica, CA 90402", 557 | "event_name": "CREATED", 558 | "id": "1c49f22b", 559 | "name": "Fried rice", 560 | "sent_at_second": 38 561 | }, 562 | { 563 | "destination": "510 18th St, Santa Monica, CA 90402", 564 | "event_name": "COOKED", 565 | "id": "1c49f22b", 566 | "name": "Fried rice", 567 | "sent_at_second": 44 568 | }, 569 | { 570 | "destination": "510 18th St, Santa Monica, CA 90402", 571 | "event_name": "DRIVER_RECEIVED", 572 | "id": "1c49f22b", 573 | "name": "Fried rice", 574 | "sent_at_second": 51 575 | }, 576 | { 577 | "destination": "510 18th St, Santa Monica, CA 90402", 578 | "event_name": "DELIVERED", 579 | "id": "1c49f22b", 580 | "name": "Fried rice", 581 | "sent_at_second": 59 582 | }, 583 | { 584 | "destination": "2406 Vanderbilt Ln, Redondo Beach, CA 90278", 585 | "event_name": "CREATED", 586 | "id": "2e9862bf", 587 | "name": "Fruit smoothie", 588 | "sent_at_second": 39 589 | }, 590 | { 591 | "destination": "2406 Vanderbilt Ln, Redondo Beach, CA 90278", 592 | "event_name": "COOKED", 593 | "id": "2e9862bf", 594 | "name": "Fruit smoothie", 595 | "sent_at_second": 47 596 | }, 597 | { 598 | "destination": "2406 Vanderbilt Ln, Redondo Beach, CA 90278", 599 | "event_name": "DRIVER_RECEIVED", 600 | "id": "2e9862bf", 601 | "name": "Fruit smoothie", 602 | "sent_at_second": 52 603 | }, 604 | { 605 | "destination": "2406 Vanderbilt Ln, Redondo Beach, CA 90278", 606 | "event_name": "DELIVERED", 607 | "id": "2e9862bf", 608 | "name": "Fruit smoothie", 609 | "sent_at_second": 57 610 | }, 611 | { 612 | "destination": "808 E Pine Ave, El Segundo, CA 90245", 613 | "event_name": "CREATED", 614 | "id": "3650294f", 615 | "name": "Green smoothie", 616 | "sent_at_second": 40 617 | }, 618 | { 619 | "destination": "808 E Pine Ave, El Segundo, CA 90245", 620 | "event_name": "COOKED", 621 | "id": "3650294f", 622 | "name": "Green smoothie", 623 | "sent_at_second": 47 624 | }, 625 | { 626 | "destination": "808 E Pine Ave, El Segundo, CA 90245", 627 | "event_name": "DRIVER_RECEIVED", 628 | "id": "3650294f", 629 | "name": "Green smoothie", 630 | "sent_at_second": 50 631 | }, 632 | { 633 | "destination": "808 E Pine Ave, El Segundo, CA 90245", 634 | "event_name": "DELIVERED", 635 | "id": "3650294f", 636 | "name": "Green smoothie", 637 | "sent_at_second": 57 638 | }, 639 | { 640 | "destination": "14622 Demblon St, Baldwin Park, CA 91706", 641 | "event_name": "CREATED", 642 | "id": "0d6bcc21", 643 | "name": "Sushi", 644 | "sent_at_second": 38 645 | }, 646 | { 647 | "destination": "14622 Demblon St, Baldwin Park, CA 91706", 648 | "event_name": "COOKED", 649 | "id": "0d6bcc21", 650 | "name": "Sushi", 651 | "sent_at_second": 47 652 | }, 653 | { 654 | "destination": "14622 Demblon St, Baldwin Park, CA 91706", 655 | "event_name": "DRIVER_RECEIVED", 656 | "id": "0d6bcc21", 657 | "name": "Sushi", 658 | "sent_at_second": 50 659 | }, 660 | { 661 | "destination": "14622 Demblon St, Baldwin Park, CA 91706", 662 | "event_name": "DELIVERED", 663 | "id": "0d6bcc21", 664 | "name": "Sushi", 665 | "sent_at_second": 54 666 | }, 667 | { 668 | "destination": "3152 Federal Ave, Los Angeles, CA 90066", 669 | "event_name": "CREATED", 670 | "id": "45b8be9d", 671 | "name": "Breakfast platter", 672 | "sent_at_second": 40 673 | }, 674 | { 675 | "destination": "3152 Federal Ave, Los Angeles, CA 90066", 676 | "event_name": "COOKED", 677 | "id": "45b8be9d", 678 | "name": "Breakfast platter", 679 | "sent_at_second": 44 680 | }, 681 | { 682 | "destination": "3152 Federal Ave, Los Angeles, CA 90066", 683 | "event_name": "DRIVER_RECEIVED", 684 | "id": "45b8be9d", 685 | "name": "Breakfast platter", 686 | "sent_at_second": 54 687 | }, 688 | { 689 | "destination": "3152 Federal Ave, Los Angeles, CA 90066", 690 | "event_name": "DELIVERED", 691 | "id": "45b8be9d", 692 | "name": "Breakfast platter", 693 | "sent_at_second": 64 694 | }, 695 | { 696 | "destination": "1112 W 57th St, Los Angeles, CA 90037", 697 | "event_name": "CREATED", 698 | "id": "7e0d72da", 699 | "name": "Vegetable kabob", 700 | "sent_at_second": 40 701 | }, 702 | { 703 | "destination": "1112 W 57th St, Los Angeles, CA 90037", 704 | "event_name": "COOKED", 705 | "id": "7e0d72da", 706 | "name": "Vegetable kabob", 707 | "sent_at_second": 45 708 | }, 709 | { 710 | "destination": "1112 W 57th St, Los Angeles, CA 90037", 711 | "event_name": "CANCELLED", 712 | "id": "7e0d72da", 713 | "name": "Vegetable kabob", 714 | "sent_at_second": 54 715 | }, 716 | { 717 | "destination": "510 18th St, Santa Monica, CA 90402", 718 | "event_name": "CREATED", 719 | "id": "e84f2cd6", 720 | "name": "Chicken kabob", 721 | "sent_at_second": 38 722 | }, 723 | { 724 | "destination": "510 18th St, Santa Monica, CA 90402", 725 | "event_name": "COOKED", 726 | "id": "e84f2cd6", 727 | "name": "Chicken kabob", 728 | "sent_at_second": 45 729 | }, 730 | { 731 | "destination": "510 18th St, Santa Monica, CA 90402", 732 | "event_name": "DRIVER_RECEIVED", 733 | "id": "e84f2cd6", 734 | "name": "Chicken kabob", 735 | "sent_at_second": 52 736 | }, 737 | { 738 | "destination": "510 18th St, Santa Monica, CA 90402", 739 | "event_name": "DELIVERED", 740 | "id": "e84f2cd6", 741 | "name": "Chicken kabob", 742 | "sent_at_second": 59 743 | }, 744 | { 745 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 746 | "event_name": "CREATED", 747 | "id": "0502d2ff", 748 | "name": "Steak kabob", 749 | "sent_at_second": 38 750 | }, 751 | { 752 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 753 | "event_name": "COOKED", 754 | "id": "0502d2ff", 755 | "name": "Steak kabob", 756 | "sent_at_second": 42 757 | }, 758 | { 759 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 760 | "event_name": "DRIVER_RECEIVED", 761 | "id": "0502d2ff", 762 | "name": "Steak kabob", 763 | "sent_at_second": 45 764 | }, 765 | { 766 | "destination": "222 S Main St Apt 1701, Los Angeles, CA 90012", 767 | "event_name": "DELIVERED", 768 | "id": "0502d2ff", 769 | "name": "Steak kabob", 770 | "sent_at_second": 49 771 | } 772 | ] -------------------------------------------------------------------------------- /src/icons/Empty3.vue: -------------------------------------------------------------------------------- 1 | 505 | 506 | 509 | 510 | 511 | -------------------------------------------------------------------------------- /src/icons/Empty2.vue: -------------------------------------------------------------------------------- 1 | 584 | 585 | 588 | 589 | 590 | -------------------------------------------------------------------------------- /src/icons/Empty1.vue: -------------------------------------------------------------------------------- 1 | 504 | 505 | 508 | 509 | 510 | --------------------------------------------------------------------------------