├── 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 |
2 |
3 |
4 |
5 |
6 |
7 |
{{ text }}
8 |
9 |
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 |
22 | We're sorry but Restaurant Management doesn't work properly without JavaScript enabled. Please enable it to continue.
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/components/Dropdown.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
44 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
23 |
24 |
63 |
--------------------------------------------------------------------------------
/src/views/OrderReview.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | Dashboard
8 |
9 |
10 |
11 |
15 |
18 | INVALID ORDER
19 |
20 |
This order does not exist
21 |
22 |
23 |
24 |
28 |
29 |
30 |
31 |
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 |
2 |
3 |
4 |
8 |
11 |
12 |
13 |
Created
14 |
15 |
16 |
17 |
21 |
24 |
25 |
26 |
27 |
Cooked
28 |
29 |
30 |
31 |
35 |
38 |
39 |
40 |
41 |
Driver received
42 |
43 |
44 |
45 |
49 |
52 |
53 |
54 |
55 |
Delivered
56 |
57 |
58 |
59 |
60 |
61 |
71 |
72 |
89 |
--------------------------------------------------------------------------------
/src/components/Modal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
18 |
24 |
25 |
26 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
106 |
--------------------------------------------------------------------------------
/src/components/Notifications/Notifications.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
26 | All orders
28 |
38 | New orders only
40 |
41 |
47 |
48 |
53 |
54 |
55 |
56 |
91 |
92 |
114 |
--------------------------------------------------------------------------------
/src/components/OrderDetail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 | {{ order.name }}
8 | (ID: {{ order.id }})
10 |
15 |
16 |
27 |
37 | {{ statusMap[order.currentStatus] }}
38 |
39 |
40 |
41 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ msg }}
4 |
5 | For a guide and recipes on how to configure / customize this project,
6 | check out the
7 | vue-cli documentation .
10 |
11 |
Installed CLI Plugins
12 |
46 |
Essential Links
47 |
70 |
Ecosystem
71 |
102 |
103 |
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 |
2 |
3 |
12 |
13 |
14 |
19 |
23 |
44 |
45 |
46 |
47 |
48 | {{ notification.name }}
49 |
50 |
51 | {{ status[notification.currentStatus] }}
52 |
53 |
54 |
55 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | {{ notification.message }}
66 |
67 |
68 | {{ notification.subtext }}
69 |
70 |
71 |
72 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
116 |
117 |
123 |
--------------------------------------------------------------------------------
/src/components/OrderMap.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | MapBox access token is missing. Please add your MapBox access token to the
5 | .env file in the root directory and restart the simulation.
6 |
7 |
8 |
9 |
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 | 
7 |
8 | Order Detail
9 | 
10 |
11 | Take manual action on an order
12 | 
13 |
14 | Filtered order view
15 | 
16 |
17 | Restaurant Performance
18 | Take manual action on an order
19 | 
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 |
2 |
3 |
4 |
5 |
Orders
6 |
11 |
12 |
17 | sent within {{ sentWithin }} seconds
18 |
19 |
20 |
21 | count: {{ filteredOrders.length }}
22 |
23 |
24 | {{ statusMap[currentFilter] }}
25 |
26 |
27 |
28 |
29 |
30 |
Sent within (seconds)
31 |
32 |
40 |
41 |
42 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | Order name
64 |
65 |
66 | Order status
67 |
68 |
69 | Destination
70 |
71 |
72 | Order created at
73 |
74 |
75 |
84 |
{{ order.name }}
85 |
86 | {{ order.currentStatus }}
87 |
88 |
89 | {{ order.destination }}
90 |
91 |
92 | {{ order.sent_at_second }} seconds
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
106 |
107 |
108 |
109 |
113 | Close
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
199 |
200 |
205 |
--------------------------------------------------------------------------------
/src/icons/Empty4.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 | empty_cart
9 |
14 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
44 |
45 |
46 |
47 |
52 |
56 |
60 |
67 |
74 |
81 |
89 |
97 |
98 |
103 |
108 |
113 |
118 |
123 |
124 |
129 |
134 |
139 |
144 |
149 |
154 |
159 |
164 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
190 |
191 |
192 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Restaurant Overview
7 |
8 |
13 |
New orders
14 |
15 | {{ $store.getters.getOrdersByFilter("CREATED").length }}
16 |
17 |
18 |
23 |
Cooked
24 |
25 | {{ $store.getters.getOrdersByFilter("COOKED").length }}
26 |
27 |
28 |
33 |
Drivers en route
34 |
35 | {{ $store.getters.getOrdersByFilter("DRIVER_RECEIVED").length }}
36 |
37 |
38 |
43 |
Delivered
44 |
45 | {{ $store.getters.getOrdersByFilter("DELIVERED").length }}
46 |
47 |
48 |
53 |
Cancelled
54 |
55 | {{ $store.getters.getOrdersByFilter("CANCELLED").length }}
56 |
57 |
58 |
59 |
60 |
61 |
70 |
71 |
72 |
73 |
74 |
Best sellers
75 |
76 |
80 |
84 |
85 |
86 |
87 |
88 |
Ingredient
89 |
# orders
90 |
91 |
92 |
97 |
{{ bestSeller[0] }}
98 |
{{ bestSeller[1] }}
99 |
100 |
101 |
102 |
103 |
104 |
Time to delivery
105 |
106 |
110 |
114 |
115 |
116 |
117 |
118 |
{{ getAverageDeliveryTime() }}
119 |
Average time to delivery (sec)
120 |
121 | from {{ $store.getters.deliveredOrders.length }} delivered
122 | orders
123 |
124 |
125 |
129 | View orders
130 |
131 |
132 |
133 |
134 |
138 |
186 |
187 |
188 |
189 |
190 |
Average order value
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
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 |
2 |
9 |
10 |
18 |
19 |
20 |
21 |
22 |
23 | blank canvas
24 |
30 |
38 |
43 |
48 |
53 |
58 |
63 |
68 |
73 |
78 |
83 |
88 |
93 |
98 |
103 |
108 |
116 |
121 |
126 |
131 |
136 |
141 |
146 |
151 |
159 |
167 |
171 |
175 |
179 |
180 |
184 |
188 |
192 |
196 |
197 |
202 |
203 |
208 |
209 |
210 |
211 |
212 |
213 |
217 |
221 |
222 |
226 |
230 |
235 |
240 |
245 |
250 |
255 |
260 |
265 |
270 |
275 |
280 |
281 |
286 |
291 |
296 |
301 |
306 |
311 |
316 |
321 |
326 |
331 |
336 |
341 |
346 |
351 |
356 |
361 |
366 |
367 |
371 |
375 |
379 |
383 |
387 |
388 |
393 |
398 |
403 |
411 |
419 |
427 |
435 |
440 |
445 |
450 |
455 |
460 |
465 |
470 |
475 |
483 |
488 |
492 |
496 |
500 |
501 |
502 |
503 |
504 |
505 |
506 |
509 |
510 |
511 |
--------------------------------------------------------------------------------
/src/icons/Empty2.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
20 |
21 |
22 |
23 | chef
24 |
32 |
38 |
46 |
54 |
55 |
56 |
57 |
61 |
62 |
63 |
64 |
65 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
85 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
134 |
135 |
136 |
137 |
142 |
147 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
179 |
187 |
195 |
203 |
204 |
205 |
210 |
215 |
220 |
225 |
230 |
235 |
240 |
245 |
250 |
255 |
256 |
261 |
266 |
271 |
276 |
281 |
286 |
291 |
299 |
307 |
315 |
323 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
349 |
354 |
359 |
364 |
369 |
374 |
379 |
384 |
389 |
394 |
399 |
404 |
409 |
414 |
419 |
424 |
429 |
434 |
439 |
444 |
445 |
449 |
453 |
457 |
461 |
465 |
466 |
471 |
476 |
481 |
486 |
491 |
496 |
501 |
506 |
511 |
515 |
520 |
525 |
526 |
531 |
535 |
539 |
543 |
548 |
556 |
561 |
569 |
574 |
582 |
583 |
584 |
585 |
588 |
589 |
590 |
--------------------------------------------------------------------------------
/src/icons/Empty1.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
20 |
21 |
22 |
23 | empty
24 |
32 |
38 |
43 |
48 |
53 |
58 |
63 |
68 |
73 |
78 |
83 |
84 |
88 |
92 |
96 |
97 |
101 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
175 |
180 |
185 |
190 |
195 |
200 |
205 |
210 |
215 |
220 |
225 |
226 |
230 |
234 |
238 |
239 |
244 |
249 |
254 |
259 |
264 |
269 |
273 |
277 |
281 |
285 |
289 |
293 |
297 |
301 |
305 |
309 |
314 |
318 |
323 |
328 |
333 |
338 |
343 |
348 |
353 |
358 |
363 |
368 |
373 |
378 |
384 |
390 |
396 |
402 |
408 |
414 |
420 |
426 |
432 |
438 |
444 |
449 |
454 |
462 |
470 |
478 |
486 |
487 |
492 |
497 |
502 |
503 |
504 |
505 |
508 |
509 |
510 |
--------------------------------------------------------------------------------