From b172144fa44b305d127f3697e187ef08d7c8fb36 Mon Sep 17 00:00:00 2001 From: M7mednsr Date: Tue, 14 Apr 2026 14:19:20 +0200 Subject: [PATCH 1/8] created v1 of dashbaord-overflow --- package-lock.json | 396 +++++++++++++++++- package.json | 1 + public/images/chef-avatar.png | Bin 0 -> 3166 bytes src/App.jsx | 26 +- src/Layout/DashboardLayout.jsx | 23 + src/Layout/index.js | 3 +- src/components/Dashboard/ChefMenuView.jsx | 169 ++++++++ src/components/Dashboard/CustomerReviews.jsx | 61 +++ src/components/Dashboard/DashboardHeader.jsx | 60 +++ src/components/Dashboard/DashboardSidebar.jsx | 87 ++++ src/components/Dashboard/DashboardView.jsx | 117 ++++++ src/components/Dashboard/InventoryAlerts.jsx | 63 +++ src/components/Dashboard/LiveKitchenView.jsx | 271 ++++++++++++ src/components/Dashboard/MetricCards.jsx | 58 +++ src/components/Dashboard/OrderTypes.jsx | 40 ++ src/components/Dashboard/OrdersOverview.jsx | 47 +++ src/components/Dashboard/OrdersView.jsx | 169 ++++++++ src/components/Dashboard/RecentActivity.jsx | 55 +++ .../Dashboard/RecipeBuilderView.jsx | 225 ++++++++++ src/components/Dashboard/RevenueChart.jsx | 52 +++ src/components/Dashboard/TopCategories.jsx | 46 ++ src/components/Dashboard/TrendingMenus.jsx | 67 +++ src/components/Favorites/FavoritesList.jsx | 2 +- src/components/ProtectedRoute.jsx | 23 +- src/components/StaffRoute.jsx | 59 +++ src/mocks/dashboardMock.js | 221 ++++++++++ src/pages/Dashboard/ChefMenu.jsx | 7 + src/pages/Dashboard/Dashboard.jsx | 7 + src/pages/Dashboard/LiveKitchen.jsx | 7 + src/pages/Dashboard/Orders.jsx | 7 + src/pages/Dashboard/RecipeBuilder.jsx | 7 + src/pages/index.js | 7 + src/services/dashboardService.js | 117 ++++++ 33 files changed, 2479 insertions(+), 21 deletions(-) create mode 100644 public/images/chef-avatar.png create mode 100644 src/Layout/DashboardLayout.jsx create mode 100644 src/components/Dashboard/ChefMenuView.jsx create mode 100644 src/components/Dashboard/CustomerReviews.jsx create mode 100644 src/components/Dashboard/DashboardHeader.jsx create mode 100644 src/components/Dashboard/DashboardSidebar.jsx create mode 100644 src/components/Dashboard/DashboardView.jsx create mode 100644 src/components/Dashboard/InventoryAlerts.jsx create mode 100644 src/components/Dashboard/LiveKitchenView.jsx create mode 100644 src/components/Dashboard/MetricCards.jsx create mode 100644 src/components/Dashboard/OrderTypes.jsx create mode 100644 src/components/Dashboard/OrdersOverview.jsx create mode 100644 src/components/Dashboard/OrdersView.jsx create mode 100644 src/components/Dashboard/RecentActivity.jsx create mode 100644 src/components/Dashboard/RecipeBuilderView.jsx create mode 100644 src/components/Dashboard/RevenueChart.jsx create mode 100644 src/components/Dashboard/TopCategories.jsx create mode 100644 src/components/Dashboard/TrendingMenus.jsx create mode 100644 src/components/StaffRoute.jsx create mode 100644 src/mocks/dashboardMock.js create mode 100644 src/pages/Dashboard/ChefMenu.jsx create mode 100644 src/pages/Dashboard/Dashboard.jsx create mode 100644 src/pages/Dashboard/LiveKitchen.jsx create mode 100644 src/pages/Dashboard/Orders.jsx create mode 100644 src/pages/Dashboard/RecipeBuilder.jsx create mode 100644 src/services/dashboardService.js diff --git a/package-lock.json b/package-lock.json index 6ad7a27..62f63bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "react-icons": "^5.5.0", "react-router": "^7.9.5", "react-router-dom": "^7.9.5", + "recharts": "^3.8.1", "tailwindcss": "^4.1.17", "zod": "^4.3.6", "zustand": "^5.0.10" @@ -1213,6 +1214,42 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.53", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", @@ -1549,7 +1586,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, "license": "MIT" }, "node_modules/@standard-schema/utils": { @@ -1951,6 +1987,69 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -1991,6 +2090,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", @@ -2380,6 +2485,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2501,6 +2615,127 @@ "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/data-urls": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", @@ -2550,6 +2785,12 @@ "dev": true, "license": "MIT" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2674,6 +2915,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -2925,6 +3176,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -3274,6 +3531,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -3301,6 +3568,15 @@ "node": ">=0.8.19" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-buffer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", @@ -4114,6 +4390,36 @@ "react": "*" } }, + "node_modules/react-is": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", + "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -4162,6 +4468,51 @@ "react-dom": ">=18" } }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -4172,6 +4523,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4366,6 +4723,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -4509,6 +4872,37 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", diff --git a/package.json b/package.json index 22d5542..ef2de1e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "react-icons": "^5.5.0", "react-router": "^7.9.5", "react-router-dom": "^7.9.5", + "recharts": "^3.8.1", "tailwindcss": "^4.1.17", "zod": "^4.3.6", "zustand": "^5.0.10" diff --git a/public/images/chef-avatar.png b/public/images/chef-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..eb129b724d1fa3ce3facd756685b103a09f23ba5 GIT binary patch literal 3166 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 Eg2;^K+5i9m literal 0 HcmV?d00001 diff --git a/src/App.jsx b/src/App.jsx index beafe0f..5976838 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,6 @@ import { Route, Routes, Navigate } from "react-router"; -import { AppLayout, AuthLayout } from "./Layout"; +import { AppLayout, AuthLayout, DashboardLayout } from "./Layout"; +import StaffRoute from "./components/StaffRoute"; import { Home, @@ -11,6 +12,11 @@ import { Thanks, Favorites, StoreDebug, + Dashboard, + Orders, + RecipeBuilder, + ChefMenu, + LiveKitchen, } from "./pages"; import { useAuthInit } from "./hooks/useAuthInit"; import Menu from "./pages/Menu/Menu"; @@ -68,6 +74,24 @@ export default function App() { {/* Catch-all: redirect unknown URLs to Home */} } /> + + {/* ── Dashboard Routes ── + Separate full-screen layout (no Navbar/Footer). + Protected: requires authentication (chef/admin). */} + + + + } + > + } /> + } /> + } /> + } /> + } /> + ); diff --git a/src/Layout/DashboardLayout.jsx b/src/Layout/DashboardLayout.jsx new file mode 100644 index 0000000..e564640 --- /dev/null +++ b/src/Layout/DashboardLayout.jsx @@ -0,0 +1,23 @@ +import { Outlet } from "react-router-dom"; +import DashboardSidebar from "../components/Dashboard/DashboardSidebar"; + +/** + * DashboardLayout + * Full-screen staff dashboard shell. + * No customer Navbar/Footer — completely separate from AppLayout. + */ +function DashboardLayout() { + return ( +
+ {/* Fixed left sidebar */} + + + {/* Scrollable main content */} +
+ +
+
+ ); +} + +export default DashboardLayout; diff --git a/src/Layout/index.js b/src/Layout/index.js index 53026af..ef6aa69 100644 --- a/src/Layout/index.js +++ b/src/Layout/index.js @@ -1,2 +1,3 @@ export { default as AppLayout } from "./AppLayout"; -export { default as AuthLayout } from "./AuthLayout"; \ No newline at end of file +export { default as AuthLayout } from "./AuthLayout"; +export { default as DashboardLayout } from "./DashboardLayout"; \ No newline at end of file diff --git a/src/components/Dashboard/ChefMenuView.jsx b/src/components/Dashboard/ChefMenuView.jsx new file mode 100644 index 0000000..960db68 --- /dev/null +++ b/src/components/Dashboard/ChefMenuView.jsx @@ -0,0 +1,169 @@ +import { useState, useEffect } from "react"; +import DashboardHeader from "../../components/Dashboard/DashboardHeader"; +import { getMenuCategories, getMenuItems } from "../../services/dashboardService"; +import { FiSearch, FiStar } from "react-icons/fi"; + +const CATEGORY_TABS = ["All", "Chicken", "Meat", "Seafood", "Vegetarian", "Desserts", "Mixed"]; + +function CategoryProgress({ name, percentage, count, color }) { + const r = 30; + const circ = 2 * Math.PI * r; + const offset = circ - (percentage / 100) * circ; + + return ( +
+
+ + + + +
+ {percentage}% +
+
+
+

{name}

+

{count} items

+
+
+ ); +} + +function ChefMenuView() { + const [categories, setCategories] = useState([]); + const [menuItems, setMenuItems] = useState([]); + const [activeTab, setActiveTab] = useState("All"); + const [search, setSearch] = useState(""); + const [loading, setLoading] = useState(true); + + useEffect(() => { + Promise.all([getMenuCategories(), getMenuItems()]) + .then(([cats, items]) => { setCategories(cats); setMenuItems(items); }) + .catch(console.error) + .finally(() => setLoading(false)); + }, []); + + const filtered = menuItems.filter((item) => { + const matchesTab = activeTab === "All" || item.category === activeTab; + const matchesSearch = item.name.toLowerCase().includes(search.toLowerCase()); + return matchesTab && matchesSearch; + }); + + return ( +
+ + +
+ {/* Category Progress Charts */} + {!loading && ( +
+ {categories.map((cat) => ( + + ))} +
+ )} + + {/* Menu Table */} +
+ {/* Table header bar */} +
+
+

All Meals

+

{filtered.length} of {menuItems.length} total

+
+ {/* Search */} +
+ + setSearch(e.target.value)} + className="border-none outline-none bg-transparent text-[13px] text-gray-700 w-40" + /> +
+
+ + {/* Category Tabs */} +
+ {CATEGORY_TABS.map((tab) => ( + + ))} +
+ + {/* Table */} +
+ + + + {["Meal Name", "Category", "Price", "Calories", "Protein", "Fat", "Sugar", "Rating", "Status"].map((h) => ( + + ))} + + + + {filtered.length === 0 && ( + + + + )} + {filtered.map((item, i) => ( + + + + + + + + + + + + ))} + +
+ {h} +
No meals found
{item.name} + + {item.category} + + ${item.price}{item.calories}{item.protein}{item.fat}{item.sugar} +
+ + {item.rating} +
+
+ + {item.status} + +
+
+
+
+
+ ); +} + +export default ChefMenuView; diff --git a/src/components/Dashboard/CustomerReviews.jsx b/src/components/Dashboard/CustomerReviews.jsx new file mode 100644 index 0000000..73ca572 --- /dev/null +++ b/src/components/Dashboard/CustomerReviews.jsx @@ -0,0 +1,61 @@ +import { FiStar } from "react-icons/fi"; + +function StarRating({ rating }) { + return ( +
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+ ); +} + +function CustomerReviews({ data }) { + return ( +
+

Customer Reviews

+
+ {data.map((review) => ( +
+ {/* Food image */} +
+ dish { e.currentTarget.style.display = "none"; }} + /> +
+ + {/* Review text */} +

{review.text}

+ + {/* Reviewer */} +
+
+
+ {review.name.charAt(0)} +
+
+

{review.name}

+ +
+
+ {review.date} +
+
+ ))} +
+
+ ); +} + +export default CustomerReviews; diff --git a/src/components/Dashboard/DashboardHeader.jsx b/src/components/Dashboard/DashboardHeader.jsx new file mode 100644 index 0000000..7d1e760 --- /dev/null +++ b/src/components/Dashboard/DashboardHeader.jsx @@ -0,0 +1,60 @@ +import { FiSearch, FiBell } from "react-icons/fi"; +import chefLogo from "../../../public/images/chef-avatar.png"; + +function DashboardHeader({ title = "Dashboard", subtitle = "Hello Basmala, Welcome back" }) { + return ( +
+ {/* Title + greeting */} +
+

{title}

+

{subtitle}

+
+ + {/* Right: search + bell + profile */} +
+ {/* Search */} +
+ + +
+ + {/* Bell */} + + + {/* Profile */} +
+
+

Chef Marc

+

Head Curator

+
+ Chef Profile { + e.currentTarget.style.display = "none"; + e.currentTarget.nextSibling.style.display = "flex"; + }} + /> + {/* Fallback avatar */} +
+ CM +
+
+
+
+ ); +} + +export default DashboardHeader; diff --git a/src/components/Dashboard/DashboardSidebar.jsx b/src/components/Dashboard/DashboardSidebar.jsx new file mode 100644 index 0000000..4699410 --- /dev/null +++ b/src/components/Dashboard/DashboardSidebar.jsx @@ -0,0 +1,87 @@ +import { NavLink } from "react-router-dom"; +import { + MdDashboard, + MdRestaurantMenu, + MdOutlineMenuBook, + MdOutlineKitchen, +} from "react-icons/md"; +import { FiShoppingBag } from "react-icons/fi"; +import { HiSparkles } from "react-icons/hi2"; + +const navItems = [ + { to: "/dashboard", label: "Dashboard", icon: MdDashboard, end: true }, + { to: "/dashboard/orders", label: "Orders", icon: FiShoppingBag }, + { to: "/dashboard/recipe-builder", label: "Recipe Builder", icon: MdRestaurantMenu }, + { to: "/dashboard/chef-menu", label: "Chef Menu", icon: MdOutlineMenuBook }, + { to: "/dashboard/live-kitchen", label: "Live Kitchen", icon: MdOutlineKitchen }, +]; + +function DashboardSidebar() { + return ( + + ); +} + +export default DashboardSidebar; diff --git a/src/components/Dashboard/DashboardView.jsx b/src/components/Dashboard/DashboardView.jsx new file mode 100644 index 0000000..a978f85 --- /dev/null +++ b/src/components/Dashboard/DashboardView.jsx @@ -0,0 +1,117 @@ +import { useState, useEffect } from "react"; +import DashboardHeader from "../../components/Dashboard/DashboardHeader"; +import MetricCards from "../../components/Dashboard/MetricCards"; +import RevenueChart from "../../components/Dashboard/RevenueChart"; +import TopCategories from "../../components/Dashboard/TopCategories"; +import OrdersOverview from "../../components/Dashboard/OrdersOverview"; +import OrderTypes from "../../components/Dashboard/OrderTypes"; +import TrendingMenus from "../../components/Dashboard/TrendingMenus"; +import InventoryAlerts from "../../components/Dashboard/InventoryAlerts"; +import RecentActivity from "../../components/Dashboard/RecentActivity"; +import CustomerReviews from "../../components/Dashboard/CustomerReviews"; +import { + getDashboardMetrics, getRevenueData, getTopCategories, + getOrdersOverview, getOrderTypes, getTrendingMenus, + getInventoryAlerts, getRecentActivity, getCustomerReviews, +} from "../../services/dashboardService"; + +function SkeletonCard({ height = 200 }) { + return ( +
+
+
+ ); +} + +function DashboardView() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + Promise.all([ + getDashboardMetrics(), getRevenueData(), getTopCategories(), + getOrdersOverview(), getOrderTypes(), getTrendingMenus(), + getInventoryAlerts(), getRecentActivity(), getCustomerReviews(), + ]) + .then(([metrics, revenue, categories, ordersOverview, orderTypes, trending, inventory, activity, reviews]) => { + setData({ metrics, revenue, categories, ordersOverview, orderTypes, trending, inventory, activity, reviews }); + }) + .catch((err) => { + console.error("[Dashboard] fetch failed:", err); + setError("Failed to load dashboard data."); + }) + .finally(() => setLoading(false)); + }, []); + + if (error) { + return ( +
+ ⚠️ +

{error}

+ +
+ ); + } + + return ( +
+ + + + +
+ {/* Row 1: Metric Cards */} + {loading ? ( +
+ +
+ ) : ( + + )} + + {/* Row 2: Revenue Chart + Top Categories */} +
+ {loading ? : } + {loading ? : } +
+ + {/* Row 3: Orders Overview + Order Types + Trending Menus */} +
+ {loading ? : } + {loading ? : } + {loading ? : } +
+ + {/* Row 4: Inventory Alerts */} + {loading ? : } + + {/* Row 5: Recent Activity + Customer Reviews */} +
+ {loading ? : } +
+ {loading ? : } +
+
+
+
+ ); +} + +export default DashboardView; diff --git a/src/components/Dashboard/InventoryAlerts.jsx b/src/components/Dashboard/InventoryAlerts.jsx new file mode 100644 index 0000000..0e7b662 --- /dev/null +++ b/src/components/Dashboard/InventoryAlerts.jsx @@ -0,0 +1,63 @@ +import { FiAlertTriangle } from "react-icons/fi"; + +function AlertItem({ emoji, name, daysLeft }) { + return ( +
+
+ {emoji} +
+ {name} + {daysLeft !== undefined && ( + ({daysLeft} day{daysLeft !== 1 ? "s" : ""} left) + )} +
+ ); +} + +const SECTION_STYLES = { + lowStock: { bg: "bg-red-50", border: "border-red-200", title: "text-red-500" }, + shelfLife: { bg: "bg-orange-50", border: "border-orange-200", title: "text-orange-500" }, + inSeason: { bg: "bg-green-50", border: "border-green-200", title: "text-green-600" }, +}; + +function InventoryAlerts({ data }) { + const sections = [ + { key: "lowStock", label: "Low Stock", items: data.lowStock, showDays: false }, + { key: "shelfLife", label: "Shelf-life: less than 3 days", items: data.shelfLife, showDays: true }, + { key: "inSeason", label: "In Season", items: data.inSeason, showDays: false }, + ]; + + return ( +
+
+ +

+ Inventory Alerts +

+
+ +
+ {sections.map(({ key, label, items, showDays }) => { + const s = SECTION_STYLES[key]; + return ( +
+

{label}

+
+ {items.map((item) => ( + + ))} +
+
+ ); + })} +
+
+ ); +} + +export default InventoryAlerts; diff --git a/src/components/Dashboard/LiveKitchenView.jsx b/src/components/Dashboard/LiveKitchenView.jsx new file mode 100644 index 0000000..92ba83a --- /dev/null +++ b/src/components/Dashboard/LiveKitchenView.jsx @@ -0,0 +1,271 @@ +import { useState, useEffect, useCallback } from "react"; +import DashboardHeader from "../../components/Dashboard/DashboardHeader"; +import { getKitchenOrders, updateOrderStatus } from "../../services/dashboardService"; +import { mockKitchenIngredients } from "../../mocks/dashboardMock"; +import { FiClock, FiList } from "react-icons/fi"; + +const COLUMNS = [ + { key: "queue", label: "Order Queue", color: "#9CA3AF", bg: "bg-gray-50", borderColor: "border-gray-300", action: "Start Preparing", nextStatus: "preparing" }, + { key: "preparing", label: "Preparing", color: "#F97316", bg: "bg-orange-50", borderColor: "border-orange-300", action: "Mark as Ready", nextStatus: "ready" }, + { key: "ready", label: "Ready", color: "#16A34A", bg: "bg-green-50", borderColor: "border-green-300", action: "Served ✓", nextStatus: "done" }, +]; + +function useTimer(startedAt) { + const [elapsed, setElapsed] = useState(0); + useEffect(() => { + if (!startedAt) return; + const interval = setInterval(() => setElapsed((e) => e + 1), 1000); + return () => clearInterval(interval); + }, [startedAt]); + const minutes = Math.floor(elapsed / 60).toString().padStart(2, "0"); + const seconds = (elapsed % 60).toString().padStart(2, "0"); + return `${minutes}:${seconds}`; +} + +function OrderCard({ order, columnKey, onAction, onSelect, isSelected }) { + const col = COLUMNS.find((c) => c.key === columnKey); + + return ( +
onSelect(order)} + className={`bg-white rounded-2xl p-4 cursor-pointer transition-all duration-200 border-l-4 ${ + isSelected + ? "ring-2 ring-orange-500 shadow-[0_4px_16px_rgba(249,115,22,0.15)]" + : "shadow-[0_1px_8px_rgba(0,0,0,0.06)]" + }`} + style={{ borderLeftColor: col.color }} + > + {/* ID + Time */} +
+ {order.id} +
+ + {order.time} +
+
+ + {/* Customer */} +

👤 {order.customer}

+ + {/* Items */} +
+ {order.items.map((item, i) => ( + • {item} + ))} +
+ + {/* Notes */} + {order.notes && ( +
+

⚠️ {order.notes}

+
+ )} + + {/* Action button */} + {col.action !== "done" && ( + + )} +
+ ); +} + +function OrderDetail({ order }) { + const timer = useTimer(order?.startedAt ? 0 : null); + const [checklist, setChecklist] = useState(mockKitchenIngredients); + + const toggleItem = useCallback((id) => { + setChecklist((prev) => prev.map((i) => i.id === id ? { ...i, checked: !i.checked } : i)); + }, []); + + if (!order) { + return ( +
+ +

Select an order to see details

+
+ ); + } + + const done = checklist.filter((i) => i.checked).length; + + return ( +
+
+
+

{order.id}

+

👤 {order.customer}

+
+ {order.startedAt && ( +
+

{timer}

+

elapsed

+
+ )} +
+ + {/* Order items */} +
+

Items

+ {order.items.map((item, i) => ( +

• {item}

+ ))} +
+ + {/* Notes */} + {order.notes && ( +
+

Special Notes

+

{order.notes}

+
+ )} + + {/* Checklist */} +
+
+

Ingredients

+ {done}/{checklist.length} +
+
+ {checklist.map((item) => ( + + ))} +
+ {/* Progress */} +
+
+
+
+
+ ); +} + +function LiveKitchenView() { + const [boards, setBoards] = useState({ queue: [], preparing: [], ready: [] }); + const [selectedOrder, setSelectedOrder] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + getKitchenOrders() + .then(setBoards) + .catch(console.error) + .finally(() => setLoading(false)); + }, []); + + const handleAction = useCallback(async (orderId, nextStatus) => { + try { + await updateOrderStatus(orderId, nextStatus); + setBoards((prev) => { + const next = { queue: [...prev.queue], preparing: [...prev.preparing], ready: [...prev.ready] }; + const statusMap = { preparing: "queue", ready: "preparing", done: "ready" }; + const fromKey = statusMap[nextStatus]; + if (!fromKey) return prev; + const toKey = nextStatus === "done" ? null : nextStatus; + const orderIdx = next[fromKey].findIndex((o) => o.id === orderId); + if (orderIdx === -1) return prev; + const [order] = next[fromKey].splice(orderIdx, 1); + if (toKey) { + order.startedAt = nextStatus === "preparing" ? new Date().toISOString() : order.startedAt; + next[toKey].push(order); + } + return next; + }); + } catch (err) { + console.error("[LiveKitchen] updateOrderStatus failed:", err); + } + }, []); + + const totalActive = boards.queue.length + boards.preparing.length + boards.ready.length; + + const statBars = [ + { label: "In Queue", value: boards.queue.length, colorCls: "text-gray-400", bg: "bg-gray-50" }, + { label: "Preparing", value: boards.preparing.length, colorCls: "text-orange-500", bg: "bg-orange-50" }, + { label: "Ready", value: boards.ready.length, colorCls: "text-green-600", bg: "bg-green-50" }, + { label: "Total Active", value: totalActive, colorCls: "text-gray-700", bg: "bg-white" }, + ]; + + return ( +
+ + +
+ {/* Stats bar */} +
+ {statBars.map(({ label, value, colorCls, bg }) => ( +
+ {value} + {label} +
+ ))} +
+ + {/* Kanban Grid */} +
+ {COLUMNS.map((col) => ( +
+ {/* Column header */} +
+
+
+ {col.label} +
+ + {loading ? "—" : boards[col.key].length} + +
+ + {/* Cards */} +
+ {!loading && boards[col.key].map((order) => ( + + ))} + {!loading && boards[col.key].length === 0 && ( +
+

Empty

+
+ )} +
+
+ ))} + + {/* Detail Panel */} +
+ +
+
+
+
+ ); +} + +export default LiveKitchenView; diff --git a/src/components/Dashboard/MetricCards.jsx b/src/components/Dashboard/MetricCards.jsx new file mode 100644 index 0000000..67b06fb --- /dev/null +++ b/src/components/Dashboard/MetricCards.jsx @@ -0,0 +1,58 @@ +import { FiTrendingUp, FiTrendingDown, FiShoppingBag, FiUsers, FiDollarSign } from "react-icons/fi"; + +const icons = { + totalOrders: FiShoppingBag, + totalCustomers: FiUsers, + totalRevenue: FiDollarSign, +}; + +const labels = { + totalOrders: "Total Orders", + totalCustomers: "Total Customers", + totalRevenue: "Total Revenue", +}; + +function formatValue(key, value) { + if (key === "totalRevenue") return `$${value.toLocaleString()}`; + return value.toLocaleString(); +} + +function MetricCards({ metrics }) { + return ( +
+ {Object.entries(metrics).map(([key, { value, change, trend }]) => { + const Icon = icons[key]; + const isUp = trend === "up"; + const TrendIcon = isUp ? FiTrendingUp : FiTrendingDown; + + return ( +
+ {/* Left: icon + label + value */} +
+
+ +
+

{labels[key]}

+

{formatValue(key, value)}

+
+ + {/* Right: trend badge */} +
+ + {Math.abs(change)}% +
+
+ ); + })} +
+ ); +} + +export default MetricCards; diff --git a/src/components/Dashboard/OrderTypes.jsx b/src/components/Dashboard/OrderTypes.jsx new file mode 100644 index 0000000..cefd9c2 --- /dev/null +++ b/src/components/Dashboard/OrderTypes.jsx @@ -0,0 +1,40 @@ +function OrderTypes({ data }) { + const icons = { "Dine-In": "🍽️", Takeaway: "🥡", Online: "📱" }; + + return ( +
+
+
+

Order Types

+

This Month

+
+
+ +
+ {data.map(({ name, percentage, count, color }) => ( +
+
+
+ {icons[name]} + {name} +
+
+ {count} + {percentage}% +
+
+ {/* Progress bar */} +
+
+
+
+ ))} +
+
+ ); +} + +export default OrderTypes; diff --git a/src/components/Dashboard/OrdersOverview.jsx b/src/components/Dashboard/OrdersOverview.jsx new file mode 100644 index 0000000..a54c746 --- /dev/null +++ b/src/components/Dashboard/OrdersOverview.jsx @@ -0,0 +1,47 @@ +import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from "recharts"; + +const CustomTooltip = ({ active, payload, label }) => { + if (active && payload && payload.length) { + return ( +
+

{label}: {payload[0].value} orders

+
+ ); + } + return null; +}; + +function OrdersOverview({ data }) { + return ( +
+
+
+

Orders Overview

+

This Week

+
+
+ + + + + + } cursor={{ fill: "rgba(249,115,22,0.05)" }} /> + + {data.map((entry, index) => ( + + ))} + + + + + {/* Highlight label */} + {data.find((d) => d.highlight) && ( +
+ 📈 Peak: {data.find((d) => d.highlight)?.day} — {data.find((d) => d.highlight)?.orders} orders +
+ )} +
+ ); +} + +export default OrdersOverview; diff --git a/src/components/Dashboard/OrdersView.jsx b/src/components/Dashboard/OrdersView.jsx new file mode 100644 index 0000000..be5b988 --- /dev/null +++ b/src/components/Dashboard/OrdersView.jsx @@ -0,0 +1,169 @@ +import { useState, useEffect } from "react"; +import DashboardHeader from "../../components/Dashboard/DashboardHeader"; +import TrendingMenus from "../../components/Dashboard/TrendingMenus"; +import { getOrdersMetrics, getOrders, getTrendingMenus } from "../../services/dashboardService"; +import { FiShoppingBag, FiClock, FiCheckCircle } from "react-icons/fi"; + +const STATUS_STYLES = { + Preparing: "bg-orange-50 text-orange-500", + Ready: "bg-sky-50 text-sky-600", + Done: "bg-green-50 text-green-600", + Cancelled: "bg-red-50 text-red-500", +}; + +const TABS = ["All", "Preparing", "Ready", "Done", "Cancelled"]; + +function CircularProgress({ current, target }) { + const pct = Math.min((current / target) * 100, 100); + const r = 52; + const circ = 2 * Math.PI * r; + const offset = circ - (pct / 100) * circ; + + return ( +
+ + + + +
+

{Math.round(pct)}%

+

Daily Goal

+
+
+

+ ${current.toLocaleString()} / ${target.toLocaleString()} +

+
+
+ ); +} + +function OrdersView() { + const [metrics, setMetrics] = useState(null); + const [orders, setOrders] = useState([]); + const [trending, setTrending] = useState([]); + const [activeTab, setActiveTab] = useState("All"); + const [loading, setLoading] = useState(true); + + useEffect(() => { + Promise.all([getOrdersMetrics(), getOrders(), getTrendingMenus()]) + .then(([m, o, t]) => { setMetrics(m); setOrders(o); setTrending(t); }) + .catch(console.error) + .finally(() => setLoading(false)); + }, []); + + const filtered = activeTab === "All" ? orders : orders.filter((o) => o.status === activeTab); + + const metricCards = metrics + ? [ + { label: "Total Orders", value: metrics.totalOrders, icon: FiShoppingBag, color: "#F97316", bgClass: "bg-orange-50" }, + { label: "Preparing", value: metrics.preparing, icon: FiClock, color: "#0284C7", bgClass: "bg-sky-50" }, + { label: "Completed", value: metrics.completed, icon: FiCheckCircle, color: "#16A34A", bgClass: "bg-green-50" }, + ] + : []; + + return ( +
+ + +
+ {/* Main content */} +
+ {/* Metric cards */} +
+ {!loading && metricCards.map(({ label, value, icon: Icon, color, bgClass }) => ( +
+
+ +
+
+

{label}

+

{value}

+
+
+ ))} +
+ + {/* Daily Goal */} + {!loading && metrics && ( +
+ +
+

Today's Revenue Progress

+

+ You're at {Math.round((metrics.dailyGoal.current / metrics.dailyGoal.target) * 100)}% of your daily goal. Keep it going! +

+
+
+ )} + + {/* Tabs + Table */} +
+
+ {TABS.map((tab) => ( + + ))} +
+ + {/* Orders Table */} +
+ + + + {["Order ID", "Time", "Order Name", "Items", "Total", "Customer", "Status"].map((h) => ( + + ))} + + + + {filtered.length === 0 && ( + + + + )} + {filtered.map((order, i) => { + const statusClass = STATUS_STYLES[order.status] || "bg-gray-100 text-gray-500"; + return ( + + + + + + + + + + ); + })} + +
+ {h} +
No orders found
{order.id}{order.time}{order.name}{order.items}${order.total.toFixed(2)}{order.customer} + + {order.status} + +
+
+
+
+ + {/* Right: Trending Menus */} + {!loading && } +
+
+ ); +} + +export default OrdersView; diff --git a/src/components/Dashboard/RecentActivity.jsx b/src/components/Dashboard/RecentActivity.jsx new file mode 100644 index 0000000..872431e --- /dev/null +++ b/src/components/Dashboard/RecentActivity.jsx @@ -0,0 +1,55 @@ +const roleColors = { + "Inventory Manager": "bg-orange-50 text-orange-500", + "Kitchen Admin": "bg-sky-50 text-sky-600", + Receptionist: "bg-green-50 text-green-600", +}; + +function RecentActivity({ data }) { + return ( +
+
+

Recent Activity

+ +
+ +
+ {data.map((item, index) => { + const roleStyle = roleColors[item.role] || "bg-gray-100 text-gray-500"; + return ( +
+ {/* Timeline line */} + {index < data.length - 1 && ( +
+ )} + + {/* Avatar */} +
+ {item.avatar} +
+ + {/* Content */} +
+
+ {item.user} + + {item.role} + +
+

{item.action}

+ {item.time} +
+
+ ); + })} +
+
+ ); +} + +export default RecentActivity; diff --git a/src/components/Dashboard/RecipeBuilderView.jsx b/src/components/Dashboard/RecipeBuilderView.jsx new file mode 100644 index 0000000..8830a6c --- /dev/null +++ b/src/components/Dashboard/RecipeBuilderView.jsx @@ -0,0 +1,225 @@ +import { useState } from "react"; +import DashboardHeader from "../../components/Dashboard/DashboardHeader"; +import { saveRecipe } from "../../services/dashboardService"; +import { mockRecipeIngredients } from "../../mocks/dashboardMock"; +import { FiPlus, FiTrash2, FiUpload } from "react-icons/fi"; + +const inputCls = "w-full px-3.5 py-2.5 border border-gray-200 rounded-[10px] text-[13px] text-gray-700 bg-white outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-colors box-border"; +const labelCls = "text-[12px] font-semibold text-gray-500 mb-1.5 block uppercase tracking-wide"; + +function RecipeBuilderView() { + const [form, setForm] = useState({ name: "", price: "", time: "", description: "", fat: "", calories: "", protein: "", sugar: "" }); + const [ingredients, setIngredients] = useState(mockRecipeIngredients); + const [newIngredient, setNewIngredient] = useState({ name: "", amount: "", emoji: "🥦" }); + const [imagePreview, setImagePreview] = useState(null); + const [saving, setSaving] = useState(false); + const [saved, setSaved] = useState(false); + + const handleChange = (field) => (e) => setForm((f) => ({ ...f, [field]: e.target.value })); + + const addIngredient = () => { + if (!newIngredient.name.trim()) return; + setIngredients((prev) => [...prev, { id: Date.now(), ...newIngredient }]); + setNewIngredient({ name: "", amount: "", emoji: "🥦" }); + }; + + const removeIngredient = (id) => setIngredients((prev) => prev.filter((i) => i.id !== id)); + + const handleImageUpload = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (ev) => setImagePreview(ev.target.result); + reader.readAsDataURL(file); + } + }; + + const handleSave = async () => { + if (!form.name.trim()) return; + setSaving(true); + try { + await saveRecipe({ ...form, ingredients }); + setSaved(true); + setTimeout(() => setSaved(false), 2000); + } catch (err) { + console.error("[RecipeBuilder] save failed:", err); + } finally { + setSaving(false); + } + }; + + const saveBtnCls = saved + ? "bg-green-600" + : saving + ? "bg-orange-400 cursor-not-allowed" + : !form.name.trim() + ? "bg-gray-200 text-gray-400 cursor-not-allowed" + : "bg-orange-500 hover:bg-orange-600 cursor-pointer"; + + return ( +
+ + +
+ {/* ── Left: Form ── */} +
+ + {/* Basic Info */} +
+

Meal Details

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +