Spaces:
Sleeping
Sleeping
update a lot
Browse files- frontend/.gitignore +3 -1
- frontend/package-lock.json +662 -0
- frontend/package.json +2 -0
- frontend/src/index.js +40 -15
- frontend/src/molecules/AdminNavBar.js +1 -0
- frontend/src/molecules/MenuItem.js +22 -10
- frontend/src/molecules/StoreItem.js +39 -13
- frontend/src/organisms/AdminReport.js +35 -0
- frontend/src/organisms/NewsSection.js +8 -1
- frontend/src/organisms/PieChartComponent.js +50 -0
- frontend/src/organisms/StackedBarChartComponent.js +59 -0
- frontend/src/organisms/SummaryReport.js +48 -0
- frontend/src/pages/AdminFeedPage.js +0 -22
- frontend/src/pages/AdminMenuPage.js +0 -21
- frontend/src/pages/CartPage.js +0 -78
- frontend/src/pages/admin-pages/AdminBranchesEditPage.js +181 -0
- frontend/src/pages/admin-pages/AdminBranchesPage.js +129 -0
- frontend/src/pages/admin-pages/AdminFeedPage.js +167 -0
- frontend/src/pages/{AdminLoginPage.js → admin-pages/AdminLoginPage.js} +3 -3
- frontend/src/pages/admin-pages/AdminMenuEditPage.js +252 -0
- frontend/src/pages/admin-pages/AdminMenuPage.js +130 -0
- frontend/src/pages/admin-pages/AdminNewsEditPage.js +177 -0
- frontend/src/pages/{AdminOrderPage.js → admin-pages/AdminOrderPage.js} +1 -1
- frontend/src/pages/{AdminSchedulePage.js → admin-pages/AdminSchedulePage.js} +1 -1
- frontend/src/pages/{AdminStaffPage.js → admin-pages/AdminStaffPage.js} +1 -1
- frontend/src/pages/{AdminSummaryPage.js → admin-pages/AdminSummaryPage.js} +8 -3
- frontend/src/pages/{AdminUserInfoPage.js → admin-pages/AdminUserInfoPage.js} +2 -2
- frontend/src/pages/user-pages/CartPage.js +217 -0
- frontend/src/pages/{HomePage.js → user-pages/HomePage.js} +5 -5
- frontend/src/pages/{LoginPage.js → user-pages/LoginPage.js} +3 -3
- frontend/src/pages/{MenuPage.js → user-pages/MenuPage.js} +149 -35
- frontend/src/pages/{NewsPage.js → user-pages/NewsPage.js} +2 -4
- frontend/src/pages/user-pages/PaymentSuccessPage.js +0 -0
- frontend/src/pages/{RegisterPage.js → user-pages/RegisterPage.js} +3 -3
- frontend/src/pages/{UserInfoPage.js → user-pages/UserInfoPage.js} +36 -34
- frontend/src/styles/styles.css +75 -1
frontend/.gitignore
CHANGED
@@ -25,4 +25,6 @@ yarn-error.log*
|
|
25 |
.env
|
26 |
note_dev.txt
|
27 |
object.json
|
28 |
-
.json
|
|
|
|
|
|
25 |
.env
|
26 |
note_dev.txt
|
27 |
object.json
|
28 |
+
test.json
|
29 |
+
data.json
|
30 |
+
# src/data/*
|
frontend/package-lock.json
CHANGED
@@ -8,11 +8,13 @@
|
|
8 |
"name": "test-app",
|
9 |
"version": "0.1.0",
|
10 |
"dependencies": {
|
|
|
11 |
"@testing-library/jest-dom": "^5.17.0",
|
12 |
"@testing-library/react": "^13.4.0",
|
13 |
"@testing-library/user-event": "^13.5.0",
|
14 |
"axios": "^1.7.7",
|
15 |
"bootstrap": "^5.3.3",
|
|
|
16 |
"js-cookie": "^3.0.5",
|
17 |
"jsonwebtoken": "^9.0.2",
|
18 |
"react": "^18.3.1",
|
@@ -3433,6 +3435,20 @@
|
|
3433 |
"node": ">= 8"
|
3434 |
}
|
3435 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3436 |
"node_modules/@pkgjs/parseargs": {
|
3437 |
"version": "0.11.0",
|
3438 |
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
@@ -4774,6 +4790,13 @@
|
|
4774 |
"integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
|
4775 |
"license": "MIT"
|
4776 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4777 |
"node_modules/@types/range-parser": {
|
4778 |
"version": "1.2.7",
|
4779 |
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
@@ -5813,6 +5836,18 @@
|
|
5813 |
"node": ">= 4.0.0"
|
5814 |
}
|
5815 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5816 |
"node_modules/autoprefixer": {
|
5817 |
"version": "10.4.20",
|
5818 |
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
@@ -6218,6 +6253,15 @@
|
|
6218 |
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
6219 |
"license": "MIT"
|
6220 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6221 |
"node_modules/batch": {
|
6222 |
"version": "0.6.1",
|
6223 |
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
@@ -6261,6 +6305,12 @@
|
|
6261 |
"url": "https://github.com/sponsors/sindresorhus"
|
6262 |
}
|
6263 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
6264 |
"node_modules/bluebird": {
|
6265 |
"version": "3.7.2",
|
6266 |
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
@@ -6431,6 +6481,18 @@
|
|
6431 |
"node-int64": "^0.4.0"
|
6432 |
}
|
6433 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6434 |
"node_modules/buffer-equal-constant-time": {
|
6435 |
"version": "1.0.1",
|
6436 |
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
@@ -6555,6 +6617,33 @@
|
|
6555 |
],
|
6556 |
"license": "CC-BY-4.0"
|
6557 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6558 |
"node_modules/case-sensitive-paths-webpack-plugin": {
|
6559 |
"version": "2.4.0",
|
6560 |
"resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz",
|
@@ -7032,6 +7121,15 @@
|
|
7032 |
"postcss": "^8.4"
|
7033 |
}
|
7034 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7035 |
"node_modules/css-loader": {
|
7036 |
"version": "6.11.0",
|
7037 |
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz",
|
@@ -7365,6 +7463,416 @@
|
|
7365 |
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
7366 |
"license": "MIT"
|
7367 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7368 |
"node_modules/damerau-levenshtein": {
|
7369 |
"version": "1.0.8",
|
7370 |
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
@@ -7567,6 +8075,15 @@
|
|
7567 |
"url": "https://github.com/sponsors/ljharb"
|
7568 |
}
|
7569 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7570 |
"node_modules/delayed-stream": {
|
7571 |
"version": "1.0.0",
|
7572 |
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
@@ -7796,6 +8313,13 @@
|
|
7796 |
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
7797 |
}
|
7798 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7799 |
"node_modules/domutils": {
|
7800 |
"version": "2.8.0",
|
7801 |
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
|
@@ -8149,6 +8673,12 @@
|
|
8149 |
"url": "https://github.com/sponsors/ljharb"
|
8150 |
}
|
8151 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
8152 |
"node_modules/escalade": {
|
8153 |
"version": "3.2.0",
|
8154 |
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
@@ -9132,6 +9662,12 @@
|
|
9132 |
"bser": "2.1.1"
|
9133 |
}
|
9134 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
9135 |
"node_modules/file-entry-cache": {
|
9136 |
"version": "6.0.1",
|
9137 |
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
@@ -10149,6 +10685,30 @@
|
|
10149 |
}
|
10150 |
}
|
10151 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10152 |
"node_modules/htmlparser2": {
|
10153 |
"version": "6.1.0",
|
10154 |
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
@@ -10430,6 +10990,24 @@
|
|
10430 |
"node": ">= 0.4"
|
10431 |
}
|
10432 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10433 |
"node_modules/invariant": {
|
10434 |
"version": "2.2.4",
|
10435 |
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
@@ -10957,6 +11535,12 @@
|
|
10957 |
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
10958 |
"license": "ISC"
|
10959 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
10960 |
"node_modules/istanbul-lib-coverage": {
|
10961 |
"version": "3.2.2",
|
10962 |
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
@@ -13425,6 +14009,24 @@
|
|
13425 |
"npm": ">=6"
|
13426 |
}
|
13427 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13428 |
"node_modules/jsx-ast-utils": {
|
13429 |
"version": "3.3.5",
|
13430 |
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
@@ -17002,6 +17604,16 @@
|
|
17002 |
"node": ">=0.10.0"
|
17003 |
}
|
17004 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17005 |
"node_modules/rimraf": {
|
17006 |
"version": "3.0.2",
|
17007 |
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
@@ -17018,6 +17630,12 @@
|
|
17018 |
"url": "https://github.com/sponsors/isaacs"
|
17019 |
}
|
17020 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
17021 |
"node_modules/rollup": {
|
17022 |
"version": "2.79.2",
|
17023 |
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
@@ -17116,6 +17734,12 @@
|
|
17116 |
"queue-microtask": "^1.2.2"
|
17117 |
}
|
17118 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
17119 |
"node_modules/safe-array-concat": {
|
17120 |
"version": "1.1.2",
|
17121 |
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
|
@@ -17735,6 +18359,16 @@
|
|
17735 |
"node": ">=8"
|
17736 |
}
|
17737 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17738 |
"node_modules/stackframe": {
|
17739 |
"version": "1.3.4",
|
17740 |
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
|
@@ -18291,6 +18925,16 @@
|
|
18291 |
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
|
18292 |
"license": "MIT"
|
18293 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18294 |
"node_modules/svgo": {
|
18295 |
"version": "1.3.2",
|
18296 |
"resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
|
@@ -18575,6 +19219,15 @@
|
|
18575 |
"node": ">=8"
|
18576 |
}
|
18577 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18578 |
"node_modules/text-table": {
|
18579 |
"version": "0.2.0",
|
18580 |
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
@@ -19113,6 +19766,15 @@
|
|
19113 |
"node": ">= 0.4.0"
|
19114 |
}
|
19115 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19116 |
"node_modules/uuid": {
|
19117 |
"version": "8.3.2",
|
19118 |
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
|
|
8 |
"name": "test-app",
|
9 |
"version": "0.1.0",
|
10 |
"dependencies": {
|
11 |
+
"@observablehq/plot": "^0.6.16",
|
12 |
"@testing-library/jest-dom": "^5.17.0",
|
13 |
"@testing-library/react": "^13.4.0",
|
14 |
"@testing-library/user-event": "^13.5.0",
|
15 |
"axios": "^1.7.7",
|
16 |
"bootstrap": "^5.3.3",
|
17 |
+
"html2pdf.js": "^0.10.2",
|
18 |
"js-cookie": "^3.0.5",
|
19 |
"jsonwebtoken": "^9.0.2",
|
20 |
"react": "^18.3.1",
|
|
|
3435 |
"node": ">= 8"
|
3436 |
}
|
3437 |
},
|
3438 |
+
"node_modules/@observablehq/plot": {
|
3439 |
+
"version": "0.6.16",
|
3440 |
+
"resolved": "https://registry.npmjs.org/@observablehq/plot/-/plot-0.6.16.tgz",
|
3441 |
+
"integrity": "sha512-LRi9Rn93yUx90MIo2Md7+vazxO3Wiat14but2ttCER0xVS+jnfoUjuCGoz6H7bz/lgI9CFcW0HWlvWjMFjAv8g==",
|
3442 |
+
"license": "ISC",
|
3443 |
+
"dependencies": {
|
3444 |
+
"d3": "^7.9.0",
|
3445 |
+
"interval-tree-1d": "^1.0.0",
|
3446 |
+
"isoformat": "^0.2.0"
|
3447 |
+
},
|
3448 |
+
"engines": {
|
3449 |
+
"node": ">=12"
|
3450 |
+
}
|
3451 |
+
},
|
3452 |
"node_modules/@pkgjs/parseargs": {
|
3453 |
"version": "0.11.0",
|
3454 |
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
|
|
4790 |
"integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
|
4791 |
"license": "MIT"
|
4792 |
},
|
4793 |
+
"node_modules/@types/raf": {
|
4794 |
+
"version": "3.4.3",
|
4795 |
+
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
4796 |
+
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
4797 |
+
"license": "MIT",
|
4798 |
+
"optional": true
|
4799 |
+
},
|
4800 |
"node_modules/@types/range-parser": {
|
4801 |
"version": "1.2.7",
|
4802 |
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
|
|
5836 |
"node": ">= 4.0.0"
|
5837 |
}
|
5838 |
},
|
5839 |
+
"node_modules/atob": {
|
5840 |
+
"version": "2.1.2",
|
5841 |
+
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
5842 |
+
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
5843 |
+
"license": "(MIT OR Apache-2.0)",
|
5844 |
+
"bin": {
|
5845 |
+
"atob": "bin/atob.js"
|
5846 |
+
},
|
5847 |
+
"engines": {
|
5848 |
+
"node": ">= 4.5.0"
|
5849 |
+
}
|
5850 |
+
},
|
5851 |
"node_modules/autoprefixer": {
|
5852 |
"version": "10.4.20",
|
5853 |
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
|
|
6253 |
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
6254 |
"license": "MIT"
|
6255 |
},
|
6256 |
+
"node_modules/base64-arraybuffer": {
|
6257 |
+
"version": "1.0.2",
|
6258 |
+
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
6259 |
+
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
6260 |
+
"license": "MIT",
|
6261 |
+
"engines": {
|
6262 |
+
"node": ">= 0.6.0"
|
6263 |
+
}
|
6264 |
+
},
|
6265 |
"node_modules/batch": {
|
6266 |
"version": "0.6.1",
|
6267 |
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
|
|
6305 |
"url": "https://github.com/sponsors/sindresorhus"
|
6306 |
}
|
6307 |
},
|
6308 |
+
"node_modules/binary-search-bounds": {
|
6309 |
+
"version": "2.0.5",
|
6310 |
+
"resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz",
|
6311 |
+
"integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==",
|
6312 |
+
"license": "MIT"
|
6313 |
+
},
|
6314 |
"node_modules/bluebird": {
|
6315 |
"version": "3.7.2",
|
6316 |
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
|
|
6481 |
"node-int64": "^0.4.0"
|
6482 |
}
|
6483 |
},
|
6484 |
+
"node_modules/btoa": {
|
6485 |
+
"version": "1.2.1",
|
6486 |
+
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
6487 |
+
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
|
6488 |
+
"license": "(MIT OR Apache-2.0)",
|
6489 |
+
"bin": {
|
6490 |
+
"btoa": "bin/btoa.js"
|
6491 |
+
},
|
6492 |
+
"engines": {
|
6493 |
+
"node": ">= 0.4.0"
|
6494 |
+
}
|
6495 |
+
},
|
6496 |
"node_modules/buffer-equal-constant-time": {
|
6497 |
"version": "1.0.1",
|
6498 |
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
|
|
6617 |
],
|
6618 |
"license": "CC-BY-4.0"
|
6619 |
},
|
6620 |
+
"node_modules/canvg": {
|
6621 |
+
"version": "3.0.10",
|
6622 |
+
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
|
6623 |
+
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
|
6624 |
+
"license": "MIT",
|
6625 |
+
"optional": true,
|
6626 |
+
"dependencies": {
|
6627 |
+
"@babel/runtime": "^7.12.5",
|
6628 |
+
"@types/raf": "^3.4.0",
|
6629 |
+
"core-js": "^3.8.3",
|
6630 |
+
"raf": "^3.4.1",
|
6631 |
+
"regenerator-runtime": "^0.13.7",
|
6632 |
+
"rgbcolor": "^1.0.1",
|
6633 |
+
"stackblur-canvas": "^2.0.0",
|
6634 |
+
"svg-pathdata": "^6.0.3"
|
6635 |
+
},
|
6636 |
+
"engines": {
|
6637 |
+
"node": ">=10.0.0"
|
6638 |
+
}
|
6639 |
+
},
|
6640 |
+
"node_modules/canvg/node_modules/regenerator-runtime": {
|
6641 |
+
"version": "0.13.11",
|
6642 |
+
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
6643 |
+
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
6644 |
+
"license": "MIT",
|
6645 |
+
"optional": true
|
6646 |
+
},
|
6647 |
"node_modules/case-sensitive-paths-webpack-plugin": {
|
6648 |
"version": "2.4.0",
|
6649 |
"resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz",
|
|
|
7121 |
"postcss": "^8.4"
|
7122 |
}
|
7123 |
},
|
7124 |
+
"node_modules/css-line-break": {
|
7125 |
+
"version": "2.1.0",
|
7126 |
+
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
7127 |
+
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
7128 |
+
"license": "MIT",
|
7129 |
+
"dependencies": {
|
7130 |
+
"utrie": "^1.0.2"
|
7131 |
+
}
|
7132 |
+
},
|
7133 |
"node_modules/css-loader": {
|
7134 |
"version": "6.11.0",
|
7135 |
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz",
|
|
|
7463 |
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
7464 |
"license": "MIT"
|
7465 |
},
|
7466 |
+
"node_modules/d3": {
|
7467 |
+
"version": "7.9.0",
|
7468 |
+
"resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
|
7469 |
+
"integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
|
7470 |
+
"license": "ISC",
|
7471 |
+
"dependencies": {
|
7472 |
+
"d3-array": "3",
|
7473 |
+
"d3-axis": "3",
|
7474 |
+
"d3-brush": "3",
|
7475 |
+
"d3-chord": "3",
|
7476 |
+
"d3-color": "3",
|
7477 |
+
"d3-contour": "4",
|
7478 |
+
"d3-delaunay": "6",
|
7479 |
+
"d3-dispatch": "3",
|
7480 |
+
"d3-drag": "3",
|
7481 |
+
"d3-dsv": "3",
|
7482 |
+
"d3-ease": "3",
|
7483 |
+
"d3-fetch": "3",
|
7484 |
+
"d3-force": "3",
|
7485 |
+
"d3-format": "3",
|
7486 |
+
"d3-geo": "3",
|
7487 |
+
"d3-hierarchy": "3",
|
7488 |
+
"d3-interpolate": "3",
|
7489 |
+
"d3-path": "3",
|
7490 |
+
"d3-polygon": "3",
|
7491 |
+
"d3-quadtree": "3",
|
7492 |
+
"d3-random": "3",
|
7493 |
+
"d3-scale": "4",
|
7494 |
+
"d3-scale-chromatic": "3",
|
7495 |
+
"d3-selection": "3",
|
7496 |
+
"d3-shape": "3",
|
7497 |
+
"d3-time": "3",
|
7498 |
+
"d3-time-format": "4",
|
7499 |
+
"d3-timer": "3",
|
7500 |
+
"d3-transition": "3",
|
7501 |
+
"d3-zoom": "3"
|
7502 |
+
},
|
7503 |
+
"engines": {
|
7504 |
+
"node": ">=12"
|
7505 |
+
}
|
7506 |
+
},
|
7507 |
+
"node_modules/d3-array": {
|
7508 |
+
"version": "3.2.4",
|
7509 |
+
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
7510 |
+
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
7511 |
+
"license": "ISC",
|
7512 |
+
"dependencies": {
|
7513 |
+
"internmap": "1 - 2"
|
7514 |
+
},
|
7515 |
+
"engines": {
|
7516 |
+
"node": ">=12"
|
7517 |
+
}
|
7518 |
+
},
|
7519 |
+
"node_modules/d3-axis": {
|
7520 |
+
"version": "3.0.0",
|
7521 |
+
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
|
7522 |
+
"integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
|
7523 |
+
"license": "ISC",
|
7524 |
+
"engines": {
|
7525 |
+
"node": ">=12"
|
7526 |
+
}
|
7527 |
+
},
|
7528 |
+
"node_modules/d3-brush": {
|
7529 |
+
"version": "3.0.0",
|
7530 |
+
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
|
7531 |
+
"integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
|
7532 |
+
"license": "ISC",
|
7533 |
+
"dependencies": {
|
7534 |
+
"d3-dispatch": "1 - 3",
|
7535 |
+
"d3-drag": "2 - 3",
|
7536 |
+
"d3-interpolate": "1 - 3",
|
7537 |
+
"d3-selection": "3",
|
7538 |
+
"d3-transition": "3"
|
7539 |
+
},
|
7540 |
+
"engines": {
|
7541 |
+
"node": ">=12"
|
7542 |
+
}
|
7543 |
+
},
|
7544 |
+
"node_modules/d3-chord": {
|
7545 |
+
"version": "3.0.1",
|
7546 |
+
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
|
7547 |
+
"integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
|
7548 |
+
"license": "ISC",
|
7549 |
+
"dependencies": {
|
7550 |
+
"d3-path": "1 - 3"
|
7551 |
+
},
|
7552 |
+
"engines": {
|
7553 |
+
"node": ">=12"
|
7554 |
+
}
|
7555 |
+
},
|
7556 |
+
"node_modules/d3-color": {
|
7557 |
+
"version": "3.1.0",
|
7558 |
+
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
7559 |
+
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
7560 |
+
"license": "ISC",
|
7561 |
+
"engines": {
|
7562 |
+
"node": ">=12"
|
7563 |
+
}
|
7564 |
+
},
|
7565 |
+
"node_modules/d3-contour": {
|
7566 |
+
"version": "4.0.2",
|
7567 |
+
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
|
7568 |
+
"integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
|
7569 |
+
"license": "ISC",
|
7570 |
+
"dependencies": {
|
7571 |
+
"d3-array": "^3.2.0"
|
7572 |
+
},
|
7573 |
+
"engines": {
|
7574 |
+
"node": ">=12"
|
7575 |
+
}
|
7576 |
+
},
|
7577 |
+
"node_modules/d3-delaunay": {
|
7578 |
+
"version": "6.0.4",
|
7579 |
+
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
7580 |
+
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
|
7581 |
+
"license": "ISC",
|
7582 |
+
"dependencies": {
|
7583 |
+
"delaunator": "5"
|
7584 |
+
},
|
7585 |
+
"engines": {
|
7586 |
+
"node": ">=12"
|
7587 |
+
}
|
7588 |
+
},
|
7589 |
+
"node_modules/d3-dispatch": {
|
7590 |
+
"version": "3.0.1",
|
7591 |
+
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
7592 |
+
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
7593 |
+
"license": "ISC",
|
7594 |
+
"engines": {
|
7595 |
+
"node": ">=12"
|
7596 |
+
}
|
7597 |
+
},
|
7598 |
+
"node_modules/d3-drag": {
|
7599 |
+
"version": "3.0.0",
|
7600 |
+
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
7601 |
+
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
7602 |
+
"license": "ISC",
|
7603 |
+
"dependencies": {
|
7604 |
+
"d3-dispatch": "1 - 3",
|
7605 |
+
"d3-selection": "3"
|
7606 |
+
},
|
7607 |
+
"engines": {
|
7608 |
+
"node": ">=12"
|
7609 |
+
}
|
7610 |
+
},
|
7611 |
+
"node_modules/d3-dsv": {
|
7612 |
+
"version": "3.0.1",
|
7613 |
+
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
|
7614 |
+
"integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
|
7615 |
+
"license": "ISC",
|
7616 |
+
"dependencies": {
|
7617 |
+
"commander": "7",
|
7618 |
+
"iconv-lite": "0.6",
|
7619 |
+
"rw": "1"
|
7620 |
+
},
|
7621 |
+
"bin": {
|
7622 |
+
"csv2json": "bin/dsv2json.js",
|
7623 |
+
"csv2tsv": "bin/dsv2dsv.js",
|
7624 |
+
"dsv2dsv": "bin/dsv2dsv.js",
|
7625 |
+
"dsv2json": "bin/dsv2json.js",
|
7626 |
+
"json2csv": "bin/json2dsv.js",
|
7627 |
+
"json2dsv": "bin/json2dsv.js",
|
7628 |
+
"json2tsv": "bin/json2dsv.js",
|
7629 |
+
"tsv2csv": "bin/dsv2dsv.js",
|
7630 |
+
"tsv2json": "bin/dsv2json.js"
|
7631 |
+
},
|
7632 |
+
"engines": {
|
7633 |
+
"node": ">=12"
|
7634 |
+
}
|
7635 |
+
},
|
7636 |
+
"node_modules/d3-dsv/node_modules/commander": {
|
7637 |
+
"version": "7.2.0",
|
7638 |
+
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
7639 |
+
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
7640 |
+
"license": "MIT",
|
7641 |
+
"engines": {
|
7642 |
+
"node": ">= 10"
|
7643 |
+
}
|
7644 |
+
},
|
7645 |
+
"node_modules/d3-ease": {
|
7646 |
+
"version": "3.0.1",
|
7647 |
+
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
7648 |
+
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
7649 |
+
"license": "BSD-3-Clause",
|
7650 |
+
"engines": {
|
7651 |
+
"node": ">=12"
|
7652 |
+
}
|
7653 |
+
},
|
7654 |
+
"node_modules/d3-fetch": {
|
7655 |
+
"version": "3.0.1",
|
7656 |
+
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
|
7657 |
+
"integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
|
7658 |
+
"license": "ISC",
|
7659 |
+
"dependencies": {
|
7660 |
+
"d3-dsv": "1 - 3"
|
7661 |
+
},
|
7662 |
+
"engines": {
|
7663 |
+
"node": ">=12"
|
7664 |
+
}
|
7665 |
+
},
|
7666 |
+
"node_modules/d3-force": {
|
7667 |
+
"version": "3.0.0",
|
7668 |
+
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
|
7669 |
+
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
|
7670 |
+
"license": "ISC",
|
7671 |
+
"dependencies": {
|
7672 |
+
"d3-dispatch": "1 - 3",
|
7673 |
+
"d3-quadtree": "1 - 3",
|
7674 |
+
"d3-timer": "1 - 3"
|
7675 |
+
},
|
7676 |
+
"engines": {
|
7677 |
+
"node": ">=12"
|
7678 |
+
}
|
7679 |
+
},
|
7680 |
+
"node_modules/d3-format": {
|
7681 |
+
"version": "3.1.0",
|
7682 |
+
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
7683 |
+
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
7684 |
+
"license": "ISC",
|
7685 |
+
"engines": {
|
7686 |
+
"node": ">=12"
|
7687 |
+
}
|
7688 |
+
},
|
7689 |
+
"node_modules/d3-geo": {
|
7690 |
+
"version": "3.1.1",
|
7691 |
+
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
|
7692 |
+
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
|
7693 |
+
"license": "ISC",
|
7694 |
+
"dependencies": {
|
7695 |
+
"d3-array": "2.5.0 - 3"
|
7696 |
+
},
|
7697 |
+
"engines": {
|
7698 |
+
"node": ">=12"
|
7699 |
+
}
|
7700 |
+
},
|
7701 |
+
"node_modules/d3-hierarchy": {
|
7702 |
+
"version": "3.1.2",
|
7703 |
+
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
|
7704 |
+
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
|
7705 |
+
"license": "ISC",
|
7706 |
+
"engines": {
|
7707 |
+
"node": ">=12"
|
7708 |
+
}
|
7709 |
+
},
|
7710 |
+
"node_modules/d3-interpolate": {
|
7711 |
+
"version": "3.0.1",
|
7712 |
+
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
7713 |
+
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
7714 |
+
"license": "ISC",
|
7715 |
+
"dependencies": {
|
7716 |
+
"d3-color": "1 - 3"
|
7717 |
+
},
|
7718 |
+
"engines": {
|
7719 |
+
"node": ">=12"
|
7720 |
+
}
|
7721 |
+
},
|
7722 |
+
"node_modules/d3-path": {
|
7723 |
+
"version": "3.1.0",
|
7724 |
+
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
7725 |
+
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
7726 |
+
"license": "ISC",
|
7727 |
+
"engines": {
|
7728 |
+
"node": ">=12"
|
7729 |
+
}
|
7730 |
+
},
|
7731 |
+
"node_modules/d3-polygon": {
|
7732 |
+
"version": "3.0.1",
|
7733 |
+
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
|
7734 |
+
"integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
|
7735 |
+
"license": "ISC",
|
7736 |
+
"engines": {
|
7737 |
+
"node": ">=12"
|
7738 |
+
}
|
7739 |
+
},
|
7740 |
+
"node_modules/d3-quadtree": {
|
7741 |
+
"version": "3.0.1",
|
7742 |
+
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
7743 |
+
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
7744 |
+
"license": "ISC",
|
7745 |
+
"engines": {
|
7746 |
+
"node": ">=12"
|
7747 |
+
}
|
7748 |
+
},
|
7749 |
+
"node_modules/d3-random": {
|
7750 |
+
"version": "3.0.1",
|
7751 |
+
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
|
7752 |
+
"integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
|
7753 |
+
"license": "ISC",
|
7754 |
+
"engines": {
|
7755 |
+
"node": ">=12"
|
7756 |
+
}
|
7757 |
+
},
|
7758 |
+
"node_modules/d3-scale": {
|
7759 |
+
"version": "4.0.2",
|
7760 |
+
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
7761 |
+
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
7762 |
+
"license": "ISC",
|
7763 |
+
"dependencies": {
|
7764 |
+
"d3-array": "2.10.0 - 3",
|
7765 |
+
"d3-format": "1 - 3",
|
7766 |
+
"d3-interpolate": "1.2.0 - 3",
|
7767 |
+
"d3-time": "2.1.1 - 3",
|
7768 |
+
"d3-time-format": "2 - 4"
|
7769 |
+
},
|
7770 |
+
"engines": {
|
7771 |
+
"node": ">=12"
|
7772 |
+
}
|
7773 |
+
},
|
7774 |
+
"node_modules/d3-scale-chromatic": {
|
7775 |
+
"version": "3.1.0",
|
7776 |
+
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
7777 |
+
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
7778 |
+
"license": "ISC",
|
7779 |
+
"dependencies": {
|
7780 |
+
"d3-color": "1 - 3",
|
7781 |
+
"d3-interpolate": "1 - 3"
|
7782 |
+
},
|
7783 |
+
"engines": {
|
7784 |
+
"node": ">=12"
|
7785 |
+
}
|
7786 |
+
},
|
7787 |
+
"node_modules/d3-selection": {
|
7788 |
+
"version": "3.0.0",
|
7789 |
+
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
7790 |
+
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
7791 |
+
"license": "ISC",
|
7792 |
+
"engines": {
|
7793 |
+
"node": ">=12"
|
7794 |
+
}
|
7795 |
+
},
|
7796 |
+
"node_modules/d3-shape": {
|
7797 |
+
"version": "3.2.0",
|
7798 |
+
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
7799 |
+
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
7800 |
+
"license": "ISC",
|
7801 |
+
"dependencies": {
|
7802 |
+
"d3-path": "^3.1.0"
|
7803 |
+
},
|
7804 |
+
"engines": {
|
7805 |
+
"node": ">=12"
|
7806 |
+
}
|
7807 |
+
},
|
7808 |
+
"node_modules/d3-time": {
|
7809 |
+
"version": "3.1.0",
|
7810 |
+
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
7811 |
+
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
7812 |
+
"license": "ISC",
|
7813 |
+
"dependencies": {
|
7814 |
+
"d3-array": "2 - 3"
|
7815 |
+
},
|
7816 |
+
"engines": {
|
7817 |
+
"node": ">=12"
|
7818 |
+
}
|
7819 |
+
},
|
7820 |
+
"node_modules/d3-time-format": {
|
7821 |
+
"version": "4.1.0",
|
7822 |
+
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
7823 |
+
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
7824 |
+
"license": "ISC",
|
7825 |
+
"dependencies": {
|
7826 |
+
"d3-time": "1 - 3"
|
7827 |
+
},
|
7828 |
+
"engines": {
|
7829 |
+
"node": ">=12"
|
7830 |
+
}
|
7831 |
+
},
|
7832 |
+
"node_modules/d3-timer": {
|
7833 |
+
"version": "3.0.1",
|
7834 |
+
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
7835 |
+
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
7836 |
+
"license": "ISC",
|
7837 |
+
"engines": {
|
7838 |
+
"node": ">=12"
|
7839 |
+
}
|
7840 |
+
},
|
7841 |
+
"node_modules/d3-transition": {
|
7842 |
+
"version": "3.0.1",
|
7843 |
+
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
7844 |
+
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
7845 |
+
"license": "ISC",
|
7846 |
+
"dependencies": {
|
7847 |
+
"d3-color": "1 - 3",
|
7848 |
+
"d3-dispatch": "1 - 3",
|
7849 |
+
"d3-ease": "1 - 3",
|
7850 |
+
"d3-interpolate": "1 - 3",
|
7851 |
+
"d3-timer": "1 - 3"
|
7852 |
+
},
|
7853 |
+
"engines": {
|
7854 |
+
"node": ">=12"
|
7855 |
+
},
|
7856 |
+
"peerDependencies": {
|
7857 |
+
"d3-selection": "2 - 3"
|
7858 |
+
}
|
7859 |
+
},
|
7860 |
+
"node_modules/d3-zoom": {
|
7861 |
+
"version": "3.0.0",
|
7862 |
+
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
7863 |
+
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
7864 |
+
"license": "ISC",
|
7865 |
+
"dependencies": {
|
7866 |
+
"d3-dispatch": "1 - 3",
|
7867 |
+
"d3-drag": "2 - 3",
|
7868 |
+
"d3-interpolate": "1 - 3",
|
7869 |
+
"d3-selection": "2 - 3",
|
7870 |
+
"d3-transition": "2 - 3"
|
7871 |
+
},
|
7872 |
+
"engines": {
|
7873 |
+
"node": ">=12"
|
7874 |
+
}
|
7875 |
+
},
|
7876 |
"node_modules/damerau-levenshtein": {
|
7877 |
"version": "1.0.8",
|
7878 |
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
|
|
8075 |
"url": "https://github.com/sponsors/ljharb"
|
8076 |
}
|
8077 |
},
|
8078 |
+
"node_modules/delaunator": {
|
8079 |
+
"version": "5.0.1",
|
8080 |
+
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
|
8081 |
+
"integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
|
8082 |
+
"license": "ISC",
|
8083 |
+
"dependencies": {
|
8084 |
+
"robust-predicates": "^3.0.2"
|
8085 |
+
}
|
8086 |
+
},
|
8087 |
"node_modules/delayed-stream": {
|
8088 |
"version": "1.0.0",
|
8089 |
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
|
|
8313 |
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
8314 |
}
|
8315 |
},
|
8316 |
+
"node_modules/dompurify": {
|
8317 |
+
"version": "2.5.7",
|
8318 |
+
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.7.tgz",
|
8319 |
+
"integrity": "sha512-2q4bEI+coQM8f5ez7kt2xclg1XsecaV9ASJk/54vwlfRRNQfDqJz2pzQ8t0Ix/ToBpXlVjrRIx7pFC/o8itG2Q==",
|
8320 |
+
"license": "(MPL-2.0 OR Apache-2.0)",
|
8321 |
+
"optional": true
|
8322 |
+
},
|
8323 |
"node_modules/domutils": {
|
8324 |
"version": "2.8.0",
|
8325 |
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
|
|
|
8673 |
"url": "https://github.com/sponsors/ljharb"
|
8674 |
}
|
8675 |
},
|
8676 |
+
"node_modules/es6-promise": {
|
8677 |
+
"version": "4.2.8",
|
8678 |
+
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
8679 |
+
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
|
8680 |
+
"license": "MIT"
|
8681 |
+
},
|
8682 |
"node_modules/escalade": {
|
8683 |
"version": "3.2.0",
|
8684 |
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
|
|
9662 |
"bser": "2.1.1"
|
9663 |
}
|
9664 |
},
|
9665 |
+
"node_modules/fflate": {
|
9666 |
+
"version": "0.8.2",
|
9667 |
+
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
9668 |
+
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
9669 |
+
"license": "MIT"
|
9670 |
+
},
|
9671 |
"node_modules/file-entry-cache": {
|
9672 |
"version": "6.0.1",
|
9673 |
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
|
|
10685 |
}
|
10686 |
}
|
10687 |
},
|
10688 |
+
"node_modules/html2canvas": {
|
10689 |
+
"version": "1.4.1",
|
10690 |
+
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
10691 |
+
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
10692 |
+
"license": "MIT",
|
10693 |
+
"dependencies": {
|
10694 |
+
"css-line-break": "^2.1.0",
|
10695 |
+
"text-segmentation": "^1.0.3"
|
10696 |
+
},
|
10697 |
+
"engines": {
|
10698 |
+
"node": ">=8.0.0"
|
10699 |
+
}
|
10700 |
+
},
|
10701 |
+
"node_modules/html2pdf.js": {
|
10702 |
+
"version": "0.10.2",
|
10703 |
+
"resolved": "https://registry.npmjs.org/html2pdf.js/-/html2pdf.js-0.10.2.tgz",
|
10704 |
+
"integrity": "sha512-WyHVeMb18Bp7vYTmBv1GVsThH//K7SRfHdSdhHPkl4JvyQarNQXnailkYn0QUbRRmnN5rdbbmSIGEsPZtzPy2Q==",
|
10705 |
+
"license": "MIT",
|
10706 |
+
"dependencies": {
|
10707 |
+
"es6-promise": "^4.2.5",
|
10708 |
+
"html2canvas": "^1.0.0",
|
10709 |
+
"jspdf": "^2.3.1"
|
10710 |
+
}
|
10711 |
+
},
|
10712 |
"node_modules/htmlparser2": {
|
10713 |
"version": "6.1.0",
|
10714 |
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
|
|
10990 |
"node": ">= 0.4"
|
10991 |
}
|
10992 |
},
|
10993 |
+
"node_modules/internmap": {
|
10994 |
+
"version": "2.0.3",
|
10995 |
+
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
10996 |
+
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
10997 |
+
"license": "ISC",
|
10998 |
+
"engines": {
|
10999 |
+
"node": ">=12"
|
11000 |
+
}
|
11001 |
+
},
|
11002 |
+
"node_modules/interval-tree-1d": {
|
11003 |
+
"version": "1.0.4",
|
11004 |
+
"resolved": "https://registry.npmjs.org/interval-tree-1d/-/interval-tree-1d-1.0.4.tgz",
|
11005 |
+
"integrity": "sha512-wY8QJH+6wNI0uh4pDQzMvl+478Qh7Rl4qLmqiluxALlNvl+I+o5x38Pw3/z7mDPTPS1dQalZJXsmbvxx5gclhQ==",
|
11006 |
+
"license": "MIT",
|
11007 |
+
"dependencies": {
|
11008 |
+
"binary-search-bounds": "^2.0.0"
|
11009 |
+
}
|
11010 |
+
},
|
11011 |
"node_modules/invariant": {
|
11012 |
"version": "2.2.4",
|
11013 |
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
|
|
11535 |
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
11536 |
"license": "ISC"
|
11537 |
},
|
11538 |
+
"node_modules/isoformat": {
|
11539 |
+
"version": "0.2.1",
|
11540 |
+
"resolved": "https://registry.npmjs.org/isoformat/-/isoformat-0.2.1.tgz",
|
11541 |
+
"integrity": "sha512-tFLRAygk9NqrRPhJSnNGh7g7oaVWDwR0wKh/GM2LgmPa50Eg4UfyaCO4I8k6EqJHl1/uh2RAD6g06n5ygEnrjQ==",
|
11542 |
+
"license": "ISC"
|
11543 |
+
},
|
11544 |
"node_modules/istanbul-lib-coverage": {
|
11545 |
"version": "3.2.2",
|
11546 |
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
|
|
14009 |
"npm": ">=6"
|
14010 |
}
|
14011 |
},
|
14012 |
+
"node_modules/jspdf": {
|
14013 |
+
"version": "2.5.2",
|
14014 |
+
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
|
14015 |
+
"integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
|
14016 |
+
"license": "MIT",
|
14017 |
+
"dependencies": {
|
14018 |
+
"@babel/runtime": "^7.23.2",
|
14019 |
+
"atob": "^2.1.2",
|
14020 |
+
"btoa": "^1.2.1",
|
14021 |
+
"fflate": "^0.8.1"
|
14022 |
+
},
|
14023 |
+
"optionalDependencies": {
|
14024 |
+
"canvg": "^3.0.6",
|
14025 |
+
"core-js": "^3.6.0",
|
14026 |
+
"dompurify": "^2.5.4",
|
14027 |
+
"html2canvas": "^1.0.0-rc.5"
|
14028 |
+
}
|
14029 |
+
},
|
14030 |
"node_modules/jsx-ast-utils": {
|
14031 |
"version": "3.3.5",
|
14032 |
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
|
|
17604 |
"node": ">=0.10.0"
|
17605 |
}
|
17606 |
},
|
17607 |
+
"node_modules/rgbcolor": {
|
17608 |
+
"version": "1.0.1",
|
17609 |
+
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
17610 |
+
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
17611 |
+
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
17612 |
+
"optional": true,
|
17613 |
+
"engines": {
|
17614 |
+
"node": ">= 0.8.15"
|
17615 |
+
}
|
17616 |
+
},
|
17617 |
"node_modules/rimraf": {
|
17618 |
"version": "3.0.2",
|
17619 |
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
|
|
17630 |
"url": "https://github.com/sponsors/isaacs"
|
17631 |
}
|
17632 |
},
|
17633 |
+
"node_modules/robust-predicates": {
|
17634 |
+
"version": "3.0.2",
|
17635 |
+
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
|
17636 |
+
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
|
17637 |
+
"license": "Unlicense"
|
17638 |
+
},
|
17639 |
"node_modules/rollup": {
|
17640 |
"version": "2.79.2",
|
17641 |
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
|
|
17734 |
"queue-microtask": "^1.2.2"
|
17735 |
}
|
17736 |
},
|
17737 |
+
"node_modules/rw": {
|
17738 |
+
"version": "1.3.3",
|
17739 |
+
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
17740 |
+
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
17741 |
+
"license": "BSD-3-Clause"
|
17742 |
+
},
|
17743 |
"node_modules/safe-array-concat": {
|
17744 |
"version": "1.1.2",
|
17745 |
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
|
|
|
18359 |
"node": ">=8"
|
18360 |
}
|
18361 |
},
|
18362 |
+
"node_modules/stackblur-canvas": {
|
18363 |
+
"version": "2.7.0",
|
18364 |
+
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
18365 |
+
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
18366 |
+
"license": "MIT",
|
18367 |
+
"optional": true,
|
18368 |
+
"engines": {
|
18369 |
+
"node": ">=0.1.14"
|
18370 |
+
}
|
18371 |
+
},
|
18372 |
"node_modules/stackframe": {
|
18373 |
"version": "1.3.4",
|
18374 |
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
|
|
|
18925 |
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
|
18926 |
"license": "MIT"
|
18927 |
},
|
18928 |
+
"node_modules/svg-pathdata": {
|
18929 |
+
"version": "6.0.3",
|
18930 |
+
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
18931 |
+
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
18932 |
+
"license": "MIT",
|
18933 |
+
"optional": true,
|
18934 |
+
"engines": {
|
18935 |
+
"node": ">=12.0.0"
|
18936 |
+
}
|
18937 |
+
},
|
18938 |
"node_modules/svgo": {
|
18939 |
"version": "1.3.2",
|
18940 |
"resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
|
|
|
19219 |
"node": ">=8"
|
19220 |
}
|
19221 |
},
|
19222 |
+
"node_modules/text-segmentation": {
|
19223 |
+
"version": "1.0.3",
|
19224 |
+
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
19225 |
+
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
19226 |
+
"license": "MIT",
|
19227 |
+
"dependencies": {
|
19228 |
+
"utrie": "^1.0.2"
|
19229 |
+
}
|
19230 |
+
},
|
19231 |
"node_modules/text-table": {
|
19232 |
"version": "0.2.0",
|
19233 |
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
|
|
19766 |
"node": ">= 0.4.0"
|
19767 |
}
|
19768 |
},
|
19769 |
+
"node_modules/utrie": {
|
19770 |
+
"version": "1.0.2",
|
19771 |
+
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
19772 |
+
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
19773 |
+
"license": "MIT",
|
19774 |
+
"dependencies": {
|
19775 |
+
"base64-arraybuffer": "^1.0.2"
|
19776 |
+
}
|
19777 |
+
},
|
19778 |
"node_modules/uuid": {
|
19779 |
"version": "8.3.2",
|
19780 |
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
frontend/package.json
CHANGED
@@ -3,11 +3,13 @@
|
|
3 |
"version": "0.1.0",
|
4 |
"private": true,
|
5 |
"dependencies": {
|
|
|
6 |
"@testing-library/jest-dom": "^5.17.0",
|
7 |
"@testing-library/react": "^13.4.0",
|
8 |
"@testing-library/user-event": "^13.5.0",
|
9 |
"axios": "^1.7.7",
|
10 |
"bootstrap": "^5.3.3",
|
|
|
11 |
"js-cookie": "^3.0.5",
|
12 |
"jsonwebtoken": "^9.0.2",
|
13 |
"react": "^18.3.1",
|
|
|
3 |
"version": "0.1.0",
|
4 |
"private": true,
|
5 |
"dependencies": {
|
6 |
+
"@observablehq/plot": "^0.6.16",
|
7 |
"@testing-library/jest-dom": "^5.17.0",
|
8 |
"@testing-library/react": "^13.4.0",
|
9 |
"@testing-library/user-event": "^13.5.0",
|
10 |
"axios": "^1.7.7",
|
11 |
"bootstrap": "^5.3.3",
|
12 |
+
"html2pdf.js": "^0.10.2",
|
13 |
"js-cookie": "^3.0.5",
|
14 |
"jsonwebtoken": "^9.0.2",
|
15 |
"react": "^18.3.1",
|
frontend/src/index.js
CHANGED
@@ -5,23 +5,27 @@ import 'bootstrap/dist/css/bootstrap.min.css';
|
|
5 |
import './styles/index.css';
|
6 |
import './styles/styles.css';
|
7 |
import ErrorPage from './pages/ErrorPage';
|
8 |
-
import HomePage from './pages/HomePage';
|
9 |
import reportWebVitals from './reportWebVitals';
|
10 |
-
import LoginPage from './pages/LoginPage';
|
11 |
-
import RegisterPage from './pages/RegisterPage';
|
12 |
-
import NewsPage from './pages/NewsPage';
|
13 |
-
import MenuPage from './pages/MenuPage';
|
14 |
-
import CartPage from './pages/CartPage';
|
15 |
-
import UserInfoPage from './pages/UserInfoPage';
|
16 |
|
17 |
-
import AdminSummaryPage from './pages/AdminSummaryPage';
|
18 |
-
import AdminFeedPage from './pages/AdminFeedPage';
|
19 |
-
import AdminMenuPage from './pages/AdminMenuPage';
|
20 |
-
import AdminStaffPage from './pages/AdminStaffPage';
|
21 |
-
import AdminOrderPage from './pages/AdminOrderPage';
|
22 |
-
import AdminSchedulePage from './pages/AdminSchedulePage';
|
23 |
-
import AdminLoginPage from './pages/AdminLoginPage';
|
24 |
-
import AdminUserInfoPage from './pages/AdminUserInfoPage';
|
|
|
|
|
|
|
|
|
25 |
|
26 |
const router = createBrowserRouter([
|
27 |
{
|
@@ -103,7 +107,28 @@ const router = createBrowserRouter([
|
|
103 |
path:"/admin-info",
|
104 |
element: <AdminUserInfoPage/>,
|
105 |
errorElement: <ErrorPage/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
106 |
}
|
|
|
107 |
]);
|
108 |
|
109 |
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
|
5 |
import './styles/index.css';
|
6 |
import './styles/styles.css';
|
7 |
import ErrorPage from './pages/ErrorPage';
|
8 |
+
import HomePage from './pages/user-pages/HomePage';
|
9 |
import reportWebVitals from './reportWebVitals';
|
10 |
+
import LoginPage from './pages/user-pages/LoginPage';
|
11 |
+
import RegisterPage from './pages/user-pages/RegisterPage';
|
12 |
+
import NewsPage from './pages/user-pages/NewsPage';
|
13 |
+
import MenuPage from './pages/user-pages/MenuPage';
|
14 |
+
import CartPage from './pages/user-pages/CartPage';
|
15 |
+
import UserInfoPage from './pages/user-pages/UserInfoPage';
|
16 |
|
17 |
+
import AdminSummaryPage from './pages/admin-pages/AdminSummaryPage';
|
18 |
+
import AdminFeedPage from './pages/admin-pages/AdminFeedPage';
|
19 |
+
import AdminMenuPage from './pages/admin-pages/AdminMenuPage';
|
20 |
+
import AdminStaffPage from './pages/admin-pages/AdminStaffPage';
|
21 |
+
import AdminOrderPage from './pages/admin-pages/AdminOrderPage';
|
22 |
+
import AdminSchedulePage from './pages/admin-pages/AdminSchedulePage';
|
23 |
+
import AdminLoginPage from './pages/admin-pages/AdminLoginPage';
|
24 |
+
import AdminUserInfoPage from './pages/admin-pages/AdminUserInfoPage';
|
25 |
+
import AdminNewsEditPage from './pages/admin-pages/AdminNewsEditPage';
|
26 |
+
import AdminBranchPage from './pages/admin-pages/AdminBranchesPage';
|
27 |
+
import AdminBranchEditPage from './pages/admin-pages/AdminBranchesEditPage';
|
28 |
+
import AdminMenuEditPage from './pages/admin-pages/AdminMenuEditPage';
|
29 |
|
30 |
const router = createBrowserRouter([
|
31 |
{
|
|
|
107 |
path:"/admin-info",
|
108 |
element: <AdminUserInfoPage/>,
|
109 |
errorElement: <ErrorPage/>
|
110 |
+
},
|
111 |
+
{
|
112 |
+
path:"/admin-news",
|
113 |
+
element: <AdminNewsEditPage/>,
|
114 |
+
errorElement: <ErrorPage/>
|
115 |
+
},
|
116 |
+
{
|
117 |
+
path:"/admin-branchs-list",
|
118 |
+
element: <AdminBranchPage/>,
|
119 |
+
errorElement: <ErrorPage/>
|
120 |
+
},
|
121 |
+
{
|
122 |
+
path:"/admin-branchs",
|
123 |
+
element: <AdminBranchEditPage/>,
|
124 |
+
errorElement: <ErrorPage/>
|
125 |
+
},
|
126 |
+
{
|
127 |
+
path:"/admin-menu-edit",
|
128 |
+
element: <AdminMenuEditPage/>,
|
129 |
+
errorElement: <ErrorPage/>
|
130 |
}
|
131 |
+
|
132 |
]);
|
133 |
|
134 |
const root = ReactDOM.createRoot(document.getElementById('root'));
|
frontend/src/molecules/AdminNavBar.js
CHANGED
@@ -62,6 +62,7 @@ export default function AdminNavbar() {
|
|
62 |
<Nav.Link disabled={!isLoggedIn} href="/admin-staff">Nhân viên</Nav.Link>
|
63 |
<Nav.Link disabled={!isLoggedIn} href="/admin-schedule">Lịch làm việc</Nav.Link>
|
64 |
<Nav.Link disabled={!isLoggedIn} href="/admin-orders">Đơn hàng</Nav.Link>
|
|
|
65 |
</Nav>
|
66 |
{userContent}
|
67 |
</Navbar.Collapse>
|
|
|
62 |
<Nav.Link disabled={!isLoggedIn} href="/admin-staff">Nhân viên</Nav.Link>
|
63 |
<Nav.Link disabled={!isLoggedIn} href="/admin-schedule">Lịch làm việc</Nav.Link>
|
64 |
<Nav.Link disabled={!isLoggedIn} href="/admin-orders">Đơn hàng</Nav.Link>
|
65 |
+
<Nav.Link disabled={!isLoggedIn} href="/admin-branchs-list">Các chi nhánh</Nav.Link>
|
66 |
</Nav>
|
67 |
{userContent}
|
68 |
</Navbar.Collapse>
|
frontend/src/molecules/MenuItem.js
CHANGED
@@ -1,16 +1,28 @@
|
|
1 |
// a card item represent a news in newsSection
|
2 |
-
import { Card } from "react-bootstrap";
|
3 |
|
4 |
-
export default function MenuItem(
|
5 |
return (
|
6 |
<Card className="align-items-center">
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
</Card>
|
15 |
-
|
16 |
}
|
|
|
1 |
// a card item represent a news in newsSection
|
2 |
+
import { Card, Button } from "react-bootstrap";
|
3 |
|
4 |
+
export default function MenuItem({ dishName, description, imageSrc, deleteable = false, delButtonCallback = null }) {
|
5 |
return (
|
6 |
<Card className="align-items-center">
|
7 |
+
<Card.Img variant="top" src={imageSrc} />
|
8 |
+
<Card.Body>
|
9 |
+
<Card.Title>{dishName}</Card.Title>
|
10 |
+
<Card.Text>
|
11 |
+
{description}
|
12 |
+
</Card.Text>
|
13 |
+
{deleteable ? (
|
14 |
+
<div className="d-flex justify-content-end pb-3">
|
15 |
+
<Button size='sm' onClick={delButtonCallback} variant='danger'
|
16 |
+
style={{
|
17 |
+
position: 'absolute',
|
18 |
+
bottom: '20px', // Khoảng cách từ dưới lên
|
19 |
+
right: '20px' // Khoảng cách từ phải qua
|
20 |
+
}}>
|
21 |
+
Xóa
|
22 |
+
</Button>
|
23 |
+
</div>
|
24 |
+
) : (<></>)}
|
25 |
+
</Card.Body>
|
26 |
</Card>
|
27 |
+
);
|
28 |
}
|
frontend/src/molecules/StoreItem.js
CHANGED
@@ -1,16 +1,42 @@
|
|
1 |
// a card item represent a news in newsSection
|
2 |
-
import { Card } from "react-bootstrap";
|
3 |
|
4 |
-
export default function StoreItem(
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
}
|
|
|
1 |
// a card item represent a news in newsSection
|
2 |
+
import { Button, Card } from "react-bootstrap";
|
3 |
|
4 |
+
export default function StoreItem({ storeName, address, imageSrc, storeHref = null, deleteable = false, delButtonCallback = null }) {
|
5 |
+
return (
|
6 |
+
<Card
|
7 |
+
as={storeHref ? 'a' : undefined}
|
8 |
+
href={storeHref ? storeHref : undefined}
|
9 |
+
style={{ height: "200px" }}
|
10 |
+
>
|
11 |
+
<Card.Img
|
12 |
+
variant="top"
|
13 |
+
src={imageSrc}
|
14 |
+
style={{ height: "200px", objectFit: 'cover', position: 'center', borderRadius: '10px' }} />
|
15 |
+
<Card.ImgOverlay
|
16 |
+
className="text-start"
|
17 |
+
style={{
|
18 |
+
backgroundImage: 'linear-gradient(rgba(20, 20, 20, 0.6), rgba(20, 20, 20, 0.5))',
|
19 |
+
// position: 'relative',
|
20 |
+
borderRadius: '10px'
|
21 |
+
}}
|
22 |
+
>
|
23 |
+
<Card.Title>{storeName}</Card.Title>
|
24 |
+
<Card.Text style={{fontWeight:'bold'}}>
|
25 |
+
{address}
|
26 |
+
</Card.Text>
|
27 |
+
{deleteable ? (
|
28 |
+
<div className="d-flex justify-content-end pb-3">
|
29 |
+
<Button size='sm' onClick={delButtonCallback} variant='danger'
|
30 |
+
style={{
|
31 |
+
position: 'absolute',
|
32 |
+
bottom: '20px', // Khoảng cách từ dưới lên
|
33 |
+
right: '20px' // Khoảng cách từ phải qua
|
34 |
+
}}>
|
35 |
+
Xóa
|
36 |
+
</Button>
|
37 |
+
</div>
|
38 |
+
) : (<></>)}
|
39 |
+
</Card.ImgOverlay>
|
40 |
+
</Card>
|
41 |
+
);
|
42 |
}
|
frontend/src/organisms/AdminReport.js
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import html2pdf from "html2pdf.js";
|
2 |
+
|
3 |
+
export default function AdminReport({ data }) {
|
4 |
+
|
5 |
+
function exportToPDF() {
|
6 |
+
const element = document.getElementById("report-admin-summary");
|
7 |
+
html2pdf().from(element).save("report.pdf");
|
8 |
+
};
|
9 |
+
|
10 |
+
return (
|
11 |
+
<div>
|
12 |
+
<button onClick={exportToPDF}> Lưu báo cáo </button>
|
13 |
+
<div id='report-admin-summary' style={{
|
14 |
+
color: 'black',
|
15 |
+
fontWeight: 'bold',
|
16 |
+
background: 'white',
|
17 |
+
alignItems: 'center',
|
18 |
+
textAlign: 'center'
|
19 |
+
}
|
20 |
+
}>
|
21 |
+
<h1>Báo cáo: {data.title}</h1>
|
22 |
+
<p>Ngày: {data.date}</p>
|
23 |
+
<ul>
|
24 |
+
{data.items.map((item, index) => (
|
25 |
+
<li key={index}>
|
26 |
+
{item.assignee} - {item.priority} - {item.summary} - {item.status}
|
27 |
+
</li>
|
28 |
+
))}
|
29 |
+
</ul>
|
30 |
+
</div>
|
31 |
+
|
32 |
+
</div>
|
33 |
+
);
|
34 |
+
|
35 |
+
}
|
frontend/src/organisms/NewsSection.js
CHANGED
@@ -9,6 +9,13 @@ function NewsSection() {
|
|
9 |
const [feeds, setFeeds] = useState([]); // Lưu danh sách bài đăng
|
10 |
const [loading, setLoading] = useState(true); // Trạng thái tải dữ liệu
|
11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
useEffect(() => {
|
13 |
const fetchFeeds = async () => {
|
14 |
try {
|
@@ -42,7 +49,7 @@ function NewsSection() {
|
|
42 |
{Array.from(feeds).map((feed, idx) => (
|
43 |
<Col key={idx}>
|
44 |
<NewsItem title={feed.title}
|
45 |
-
text={feed.description}
|
46 |
imageSrc={feed.image_url}
|
47 |
// feedHref={feed.feedHref}
|
48 |
feedHref={'/news?id='+feed.id} //demo
|
|
|
9 |
const [feeds, setFeeds] = useState([]); // Lưu danh sách bài đăng
|
10 |
const [loading, setLoading] = useState(true); // Trạng thái tải dữ liệu
|
11 |
|
12 |
+
function truncateText(text, maxLength = 100) {
|
13 |
+
if (text.length <= maxLength) {
|
14 |
+
return text;
|
15 |
+
}
|
16 |
+
return text.slice(0, maxLength) + '...';
|
17 |
+
}
|
18 |
+
|
19 |
useEffect(() => {
|
20 |
const fetchFeeds = async () => {
|
21 |
try {
|
|
|
49 |
{Array.from(feeds).map((feed, idx) => (
|
50 |
<Col key={idx}>
|
51 |
<NewsItem title={feed.title}
|
52 |
+
text={truncateText(feed.description)}
|
53 |
imageSrc={feed.image_url}
|
54 |
// feedHref={feed.feedHref}
|
55 |
feedHref={'/news?id='+feed.id} //demo
|
frontend/src/organisms/PieChartComponent.js
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as Plot from "@observablehq/plot";
|
2 |
+
import { Card } from "react-bootstrap";
|
3 |
+
import React, {useRef, useEffect} from "react";
|
4 |
+
|
5 |
+
|
6 |
+
const PieChartComponent = ({ tasks }) => {
|
7 |
+
const chartRef = useRef();
|
8 |
+
|
9 |
+
useEffect(() => {
|
10 |
+
if (chartRef.current) {
|
11 |
+
while (chartRef.current.firstChild) {
|
12 |
+
chartRef.current.removeChild(chartRef.current.firstChild);
|
13 |
+
}
|
14 |
+
|
15 |
+
const priorityCounts = tasks.reduce((acc, task) => {
|
16 |
+
acc[task.priority] = (acc[task.priority] || 0) + 1;
|
17 |
+
return acc;
|
18 |
+
}, {});
|
19 |
+
|
20 |
+
const data = Object.entries(priorityCounts).map(([priority, count]) => ({ priority, count }));
|
21 |
+
|
22 |
+
const chart = Plot.plot({
|
23 |
+
marks: [
|
24 |
+
Plot.barY(data, {
|
25 |
+
x: "priority",
|
26 |
+
y: "count",
|
27 |
+
fill: "priority",
|
28 |
+
stroke: "white",
|
29 |
+
title: d => `${d.priority}: ${d.count}`,
|
30 |
+
r: "count"
|
31 |
+
})
|
32 |
+
],
|
33 |
+
height: 500,
|
34 |
+
width: 600,
|
35 |
+
});
|
36 |
+
chartRef.current.append(chart);
|
37 |
+
}
|
38 |
+
}, [tasks]);
|
39 |
+
|
40 |
+
return (
|
41 |
+
<Card className="card-report card-nospan">
|
42 |
+
<Card.Body >
|
43 |
+
<Card.Title>Task Priority Distribution</Card.Title>
|
44 |
+
<div ref={chartRef} className="d-flex justify-content-center align-items-center" />
|
45 |
+
</Card.Body>
|
46 |
+
</Card>
|
47 |
+
);
|
48 |
+
};
|
49 |
+
|
50 |
+
export default PieChartComponent;
|
frontend/src/organisms/StackedBarChartComponent.js
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as Plot from "@observablehq/plot";
|
2 |
+
import { Card } from "react-bootstrap";
|
3 |
+
import React, {useRef, useEffect} from "react";
|
4 |
+
|
5 |
+
const StackedBarChartComponent = ({ tasks }) => {
|
6 |
+
const chartRef = useRef();
|
7 |
+
|
8 |
+
useEffect(() => {
|
9 |
+
if (chartRef.current) {
|
10 |
+
while (chartRef.current.firstChild) {
|
11 |
+
chartRef.current.removeChild(chartRef.current.firstChild);
|
12 |
+
}
|
13 |
+
|
14 |
+
const data = tasks.reduce((acc, task) => {
|
15 |
+
// Tìm object có cùng assignee và status trong accumulator
|
16 |
+
const existing = acc.find(
|
17 |
+
item => item.assignee === task.assignee && item.status === task.status
|
18 |
+
);
|
19 |
+
|
20 |
+
if (existing) {
|
21 |
+
// Nếu đã tồn tại, cộng thêm vào count
|
22 |
+
existing.count += 1;
|
23 |
+
} else {
|
24 |
+
// Nếu chưa tồn tại, thêm một object mới vào accumulator
|
25 |
+
acc.push({ assignee: task.assignee, status: task.status, count: 1 });
|
26 |
+
}
|
27 |
+
|
28 |
+
return acc;
|
29 |
+
}, []).sort((a, b) => a.status.length - b.status.length);
|
30 |
+
|
31 |
+
const chart = Plot.plot({
|
32 |
+
marks: [
|
33 |
+
Plot.barY(data, {
|
34 |
+
x: "assignee",
|
35 |
+
y: "count",
|
36 |
+
fill: "status",
|
37 |
+
sort: { x: "y", reverse: true },
|
38 |
+
stack: true,
|
39 |
+
})
|
40 |
+
],
|
41 |
+
height: 400,
|
42 |
+
width: 600,
|
43 |
+
color: { legend: true },
|
44 |
+
});
|
45 |
+
chartRef.current.append(chart);
|
46 |
+
}
|
47 |
+
}, [tasks]);
|
48 |
+
|
49 |
+
return (
|
50 |
+
<Card className="card-report card-nospan ">
|
51 |
+
<Card.Body>
|
52 |
+
<Card.Title>Task Distribution by Assignee and Status</Card.Title>
|
53 |
+
<div ref={chartRef} className="d-flex justify-content-center align-items-center" />
|
54 |
+
</Card.Body>
|
55 |
+
</Card>
|
56 |
+
);
|
57 |
+
};
|
58 |
+
|
59 |
+
export default StackedBarChartComponent;
|
frontend/src/organisms/SummaryReport.js
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Button, Container, Row, Col } from "react-bootstrap";
|
2 |
+
import html2pdf from "html2pdf.js";
|
3 |
+
import dataSource from '../data/data.json';
|
4 |
+
import PieChartComponent from "./PieChartComponent";
|
5 |
+
import StackedBarChartComponent from "./StackedBarChartComponent";
|
6 |
+
|
7 |
+
function exportToPDF() {
|
8 |
+
const element = document.getElementById("report-admin-summary");
|
9 |
+
html2pdf().from(element).save("report.pdf");
|
10 |
+
};
|
11 |
+
|
12 |
+
export default function SummaryReport() {
|
13 |
+
return (
|
14 |
+
<Container className="d-flex align-items-center justify-content-center p-4">
|
15 |
+
<Row className="justify-content-center">
|
16 |
+
<Col xs='4' className="my-5">
|
17 |
+
<Button variant="primary" onClick={exportToPDF}>
|
18 |
+
Xuất báo cáo
|
19 |
+
</Button>
|
20 |
+
</Col>
|
21 |
+
<Col xs='12'>
|
22 |
+
<div id="report-admin-summary"
|
23 |
+
style={{
|
24 |
+
color: 'black',
|
25 |
+
fontWeight: 'bold',
|
26 |
+
background: 'white',
|
27 |
+
alignItems: 'center',
|
28 |
+
textAlign: 'center',
|
29 |
+
padding: '50px',
|
30 |
+
}}
|
31 |
+
>
|
32 |
+
<h1 className="my-3">Demo report</h1>
|
33 |
+
<Row>
|
34 |
+
<Col xs='12' className="my-5">
|
35 |
+
<PieChartComponent tasks={dataSource.tasks} />
|
36 |
+
</Col>
|
37 |
+
<Col xs='12' className="my-5">
|
38 |
+
<StackedBarChartComponent tasks={dataSource.tasks} />
|
39 |
+
</Col>
|
40 |
+
</Row>
|
41 |
+
</div>
|
42 |
+
</Col>
|
43 |
+
</Row>
|
44 |
+
|
45 |
+
|
46 |
+
</Container>
|
47 |
+
);
|
48 |
+
}
|
frontend/src/pages/AdminFeedPage.js
DELETED
@@ -1,22 +0,0 @@
|
|
1 |
-
import { Container, Row, Col } from "react-bootstrap";
|
2 |
-
import AdminTemplate from "../templates/AdminTemplate";
|
3 |
-
|
4 |
-
export default function AdminFeedPage() {
|
5 |
-
|
6 |
-
return (
|
7 |
-
<AdminTemplate content={
|
8 |
-
(
|
9 |
-
<Container className='d-flex text-center align-items-center justify-content-center' style={{ minHeight: '80vh' }}>
|
10 |
-
<Row>
|
11 |
-
<Col xs={12}>
|
12 |
-
<h1>This is a demo feed page</h1>
|
13 |
-
</Col>
|
14 |
-
<Col xs={12}>
|
15 |
-
<h3>In the future, we hope to view list of feed, add, edit or remove feeds.</h3>
|
16 |
-
</Col>
|
17 |
-
</Row>
|
18 |
-
</Container>
|
19 |
-
)
|
20 |
-
} />
|
21 |
-
);
|
22 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/AdminMenuPage.js
DELETED
@@ -1,21 +0,0 @@
|
|
1 |
-
import { Container, Row, Col } from 'react-bootstrap';
|
2 |
-
import AdminTemplate from "../templates/AdminTemplate";
|
3 |
-
|
4 |
-
export default function AdminMenuPage() {
|
5 |
-
return (
|
6 |
-
<AdminTemplate content={
|
7 |
-
(
|
8 |
-
<Container className='d-flex text-center align-items-center justify-content-center' style={{ minHeight: '80vh' }}>
|
9 |
-
<Row>
|
10 |
-
<Col xs={12}>
|
11 |
-
<h1>This is a demo menu page</h1>
|
12 |
-
</Col>
|
13 |
-
<Col xs={12}>
|
14 |
-
<h3>In the future, the menu should be retrieve from master-list, and branch admins can decide which item is available at this branch</h3>
|
15 |
-
</Col>
|
16 |
-
</Row>
|
17 |
-
</Container>
|
18 |
-
)
|
19 |
-
} />
|
20 |
-
);
|
21 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/CartPage.js
DELETED
@@ -1,78 +0,0 @@
|
|
1 |
-
import BasicTemplate from "../templates/BasicTemplate";
|
2 |
-
import { Container, Row, Col, Card, Button } from "react-bootstrap";
|
3 |
-
import { useState, useEffect } from "react";
|
4 |
-
import DataStorage from "../organisms/DataStorage";
|
5 |
-
|
6 |
-
export default function CartPage() {
|
7 |
-
|
8 |
-
const [cartItems, setCartItems] = useState([]);
|
9 |
-
|
10 |
-
useEffect(() => {
|
11 |
-
// Lấy giỏ hàng từ sessionStorage
|
12 |
-
const cart = JSON.parse(DataStorage.get('cart')) || {};
|
13 |
-
|
14 |
-
// Chuyển cart thành mảng chứa các món có số lượng > 0
|
15 |
-
const items = Object.entries(cart).map(([_, item]) => ({
|
16 |
-
name: item.name,
|
17 |
-
amount: item.amount,
|
18 |
-
imageSrc: item.imageSrc,
|
19 |
-
price: item.price
|
20 |
-
}));
|
21 |
-
|
22 |
-
console.log(items);
|
23 |
-
setCartItems(items);
|
24 |
-
}, []);
|
25 |
-
|
26 |
-
return (
|
27 |
-
<BasicTemplate content={
|
28 |
-
(
|
29 |
-
<Container className="d-flex align-items-center justify-content-center my-5" style={{ minHeight: '70vh' }}>
|
30 |
-
{cartItems.length > 0 ? (
|
31 |
-
<div className="text-center">
|
32 |
-
<h2 className="text-center mb-4">Giỏ hàng của bạn</h2>
|
33 |
-
<Row className="g-3">
|
34 |
-
{cartItems.map((item, idx) => (
|
35 |
-
<Col md={12} key={idx} className="m-3">
|
36 |
-
<Card className="shadow-sm" style={{ display: 'flex', flexDirection: 'row' }}>
|
37 |
-
<Card.Img
|
38 |
-
variant="left"
|
39 |
-
src={item.imageSrc}
|
40 |
-
style={{ width: '150px', objectFit: 'cover' }}
|
41 |
-
/>
|
42 |
-
<Card.Body>
|
43 |
-
<Row xs={4} >
|
44 |
-
<Col className="d-flex align-items-center justify-content-center">
|
45 |
-
<Card.Title>{item.name}</Card.Title>
|
46 |
-
</Col>
|
47 |
-
<Col className="d-flex align-items-center justify-content-center">
|
48 |
-
<Card.Text>Đơn giá: {item.price} VND</Card.Text>
|
49 |
-
</Col>
|
50 |
-
<Col className="d-flex align-items-center justify-content-center">
|
51 |
-
<Card.Text>Số lượng: {item.amount}</Card.Text>
|
52 |
-
</Col>
|
53 |
-
<Col className="d-flex align-items-center justify-content-center">
|
54 |
-
<Card.Text>Tổng cộng: {item.price * item.amount} VND</Card.Text>
|
55 |
-
</Col>
|
56 |
-
</Row>
|
57 |
-
</Card.Body>
|
58 |
-
</Card>
|
59 |
-
</Col>
|
60 |
-
))}
|
61 |
-
</Row>
|
62 |
-
<Button as='a' href='/payment' className='m-3'>
|
63 |
-
Thanh toán
|
64 |
-
</Button>
|
65 |
-
<Button as='a' href='/menu' className="m-3">
|
66 |
-
Quay lại menu</Button>
|
67 |
-
</div>
|
68 |
-
) : (
|
69 |
-
<div className="text-center">
|
70 |
-
<p className="text-center my-3">Giỏ hàng của bạn hiện đang trống.</p>
|
71 |
-
<Button as='a' href='/menu'>Xem menu</Button>
|
72 |
-
</div>
|
73 |
-
)}
|
74 |
-
</Container>
|
75 |
-
)
|
76 |
-
} />
|
77 |
-
)
|
78 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/admin-pages/AdminBranchesEditPage.js
ADDED
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Container, Row, Col, Card, Form, Image, Alert, Button } from "react-bootstrap";
|
2 |
+
import AdminTemplate from "../../templates/AdminTemplate";
|
3 |
+
import { useSearchParams } from 'react-router-dom';
|
4 |
+
import axios from "axios";
|
5 |
+
import DataStorage from "../../organisms/DataStorage";
|
6 |
+
import { useEffect, useState } from "react";
|
7 |
+
import { useNavigate } from "react-router-dom";
|
8 |
+
|
9 |
+
export default function AdminBranchEditPage() {
|
10 |
+
|
11 |
+
const [error, setError] = useState("");
|
12 |
+
const [searchParams] = useSearchParams();
|
13 |
+
const storeId = searchParams.get('id') || null;
|
14 |
+
|
15 |
+
const [storeDetail, setStoreItem] = useState({
|
16 |
+
name:'',
|
17 |
+
location:'',
|
18 |
+
image_url:''
|
19 |
+
});
|
20 |
+
const [initialUrl, setInitialUrl] = useState("");
|
21 |
+
const [initialTitle, setInitialTitle] = useState("");
|
22 |
+
const [initialDesc, setInitialDesc] = useState("");
|
23 |
+
// const [loading, setLoading] = useState(true);
|
24 |
+
const [isChanged, setChanged] = useState(false);
|
25 |
+
|
26 |
+
const navigate = useNavigate();
|
27 |
+
|
28 |
+
useEffect(() => {
|
29 |
+
if (!DataStorage.get('isLoggedInAdmin')) {
|
30 |
+
navigate('/admin-login');
|
31 |
+
}
|
32 |
+
}, [navigate]);
|
33 |
+
|
34 |
+
const checkChange = () => {
|
35 |
+
if (storeDetail.name.trim() !== initialTitle.trim()
|
36 |
+
|| storeDetail.location.trim() !== initialDesc.trim()
|
37 |
+
|| storeDetail.image_url.trim() !== initialUrl.trim()) {
|
38 |
+
setChanged(true);
|
39 |
+
} else {
|
40 |
+
setChanged(false);
|
41 |
+
}
|
42 |
+
}
|
43 |
+
|
44 |
+
const handleChange = (e) => {
|
45 |
+
const { name, value } = e.target;
|
46 |
+
setStoreItem({ ...storeDetail, [name]: value });
|
47 |
+
checkChange();
|
48 |
+
};
|
49 |
+
|
50 |
+
const handleAvatarUrlChange = (e) => {
|
51 |
+
const url = e.target.value;
|
52 |
+
setStoreItem((prevData) => ({
|
53 |
+
...prevData,
|
54 |
+
image_url: url // Cập nhật URL ảnh
|
55 |
+
}));
|
56 |
+
setChanged(true);
|
57 |
+
};
|
58 |
+
|
59 |
+
useEffect(() => {
|
60 |
+
if (storeId) {
|
61 |
+
axios.get(process.env.REACT_APP_API_URL + `/branchs/${storeId}`)
|
62 |
+
.then((response) => {
|
63 |
+
setStoreItem(response.data);
|
64 |
+
// setLoading(false);
|
65 |
+
setInitialTitle(response.data.name);
|
66 |
+
setInitialDesc(response.data.location);
|
67 |
+
setInitialUrl(response.data.image_url);
|
68 |
+
})
|
69 |
+
.catch((error) => {
|
70 |
+
setError(JSON.stringify(error));
|
71 |
+
})
|
72 |
+
}
|
73 |
+
}, [storeId]);
|
74 |
+
|
75 |
+
const handleSubmit = () => {
|
76 |
+
const submit_data = {
|
77 |
+
'name': storeDetail.name,
|
78 |
+
'location': storeDetail.location,
|
79 |
+
'image_url': storeDetail.image_url
|
80 |
+
}
|
81 |
+
if (storeId) {
|
82 |
+
axios.patch(process.env.REACT_APP_API_URL + `/branchs/${storeId}`, submit_data)
|
83 |
+
.then((response) => {
|
84 |
+
// setFeedItem(response.data);
|
85 |
+
// // setLoading(false);
|
86 |
+
// setInitialDesc(response.data.description);
|
87 |
+
// setInitialTitle(response.data.title);
|
88 |
+
// setChanged(false);
|
89 |
+
navigate('/admin-branchs-list');
|
90 |
+
// window.location.reload();
|
91 |
+
})
|
92 |
+
.catch((error) => {
|
93 |
+
setError(JSON.stringify(error));
|
94 |
+
})
|
95 |
+
} else {
|
96 |
+
axios.post(process.env.REACT_APP_API_URL + `/branchs`, submit_data)
|
97 |
+
.then((response) => {
|
98 |
+
navigate('/admin-branchs-list');
|
99 |
+
})
|
100 |
+
.catch((error) => {
|
101 |
+
setError(JSON.stringify(error));
|
102 |
+
})
|
103 |
+
}
|
104 |
+
}
|
105 |
+
|
106 |
+
return (
|
107 |
+
<AdminTemplate content={
|
108 |
+
(
|
109 |
+
<Container fluid className="d-flex align-items-center justify-content-center mt-5">
|
110 |
+
<Row className="align-items-center">
|
111 |
+
{/* <Col xs={1} md={2}></Col> */}
|
112 |
+
<Col>
|
113 |
+
<Card style={{ width: '100%' }} className='justify-content-center'>
|
114 |
+
<Card.Header>
|
115 |
+
<Card.Title className='mt-1 text-center'>{storeId ? 'Chỉnh sửa thông tin chi nhánh' : 'Tạo chi nhánh'}</Card.Title>
|
116 |
+
</Card.Header>
|
117 |
+
<Card.Body>
|
118 |
+
|
119 |
+
<Form>
|
120 |
+
{error && <Alert variant="danger">{error}</Alert>}
|
121 |
+
<Row className="mb-3">
|
122 |
+
<Col xs={12} className="text-center mb-3">
|
123 |
+
{/* Hiển thị ảnh từ URL */}
|
124 |
+
<Image
|
125 |
+
src={storeDetail.image_url} // Hiển thị ảnh đại diện từ URL
|
126 |
+
alt="Ảnh mô tả"
|
127 |
+
width="auto"
|
128 |
+
height={400}
|
129 |
+
className="mb-3"
|
130 |
+
/>
|
131 |
+
<Form.Group controlId="formImageUrl">
|
132 |
+
<Form.Label>URL ảnh mô tả</Form.Label>
|
133 |
+
<Form.Control
|
134 |
+
type="text"
|
135 |
+
placeholder="Nhập URL ảnh"
|
136 |
+
value={storeDetail.image_url || ''}
|
137 |
+
onChange={handleAvatarUrlChange} // Gọi hàm khi URL thay đổi
|
138 |
+
/>
|
139 |
+
</Form.Group>
|
140 |
+
</Col>
|
141 |
+
<Col xs={12} className="mb-3">
|
142 |
+
<Form.Group controlId="formTitle">
|
143 |
+
<Form.Label>Tên chi nhánh</Form.Label>
|
144 |
+
<Form.Control
|
145 |
+
type="text"
|
146 |
+
name="name"
|
147 |
+
placeholder={storeId ? 'Đang tải dữ liệu...' : 'Tên chi nhánh'}
|
148 |
+
value={storeDetail.name || ''}
|
149 |
+
onChange={handleChange}
|
150 |
+
/>
|
151 |
+
</Form.Group>
|
152 |
+
</Col>
|
153 |
+
</Row>
|
154 |
+
|
155 |
+
<Form.Group controlId="formText" className="mb-3">
|
156 |
+
<Form.Label>Địa chỉ</Form.Label>
|
157 |
+
<Form.Control
|
158 |
+
as="textarea"
|
159 |
+
name="location"
|
160 |
+
placeholder={storeId ? 'Đang tải dữ liệu...' : 'Địa chỉ'}
|
161 |
+
rows={5}
|
162 |
+
value={storeDetail.location || ''}
|
163 |
+
onChange={handleChange}
|
164 |
+
/>
|
165 |
+
</Form.Group>
|
166 |
+
|
167 |
+
<Button variant="primary" type="button" disabled={!isChanged} onClick={handleSubmit}>
|
168 |
+
{storeId ? 'Cập nhật' : 'Tạo mới'}
|
169 |
+
</Button>
|
170 |
+
</Form>
|
171 |
+
</Card.Body>
|
172 |
+
</Card>
|
173 |
+
|
174 |
+
</Col>
|
175 |
+
{/* <Col xs={1} md={2}></Col> */}
|
176 |
+
</Row>
|
177 |
+
</Container>
|
178 |
+
)
|
179 |
+
} />
|
180 |
+
)
|
181 |
+
}
|
frontend/src/pages/admin-pages/AdminBranchesPage.js
ADDED
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Container, Col, Row, Button, Modal } from 'react-bootstrap';
|
2 |
+
import StoreItem from '../../molecules/StoreItem';
|
3 |
+
import axios from 'axios';
|
4 |
+
import { useState, useEffect } from 'react';
|
5 |
+
import { useNavigate } from 'react-router-dom';
|
6 |
+
import DataStorage from '../../organisms/DataStorage';
|
7 |
+
import AdminTemplate from '../../templates/AdminTemplate';
|
8 |
+
|
9 |
+
export default function AdminBranchPage() {
|
10 |
+
|
11 |
+
const [stores, setStores] = useState([]); // Lưu danh sách chi nhánh
|
12 |
+
const [loading, setLoading] = useState(true); // Trạng thái tải dữ liệu
|
13 |
+
const [selectedStore, setSelectedStore] = useState(null);
|
14 |
+
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
15 |
+
const navigate = useNavigate();
|
16 |
+
|
17 |
+
useEffect(() => {
|
18 |
+
if (!DataStorage.get('isLoggedInAdmin')) {
|
19 |
+
navigate('/admin-login');
|
20 |
+
}
|
21 |
+
}, [navigate]);
|
22 |
+
|
23 |
+
useEffect(() => {
|
24 |
+
// Gọi API lấy danh sách chi nhánh
|
25 |
+
const fetchBranches = async () => {
|
26 |
+
try {
|
27 |
+
const response = await axios.get(process.env.REACT_APP_API_URL + '/branchs'); // Thay 'API_ENDPOINT' bằng URL của API
|
28 |
+
setStores(response.data); // Lưu dữ liệu vào state
|
29 |
+
setLoading(false); // Đặt loading thành false khi hoàn tất
|
30 |
+
CacheStorage.set('stores', JSON.stringify(Object(response.data)));
|
31 |
+
} catch (error) {
|
32 |
+
console.error('Error fetching branches:', error);
|
33 |
+
setLoading(false); // Đặt loading thành false nếu lỗi
|
34 |
+
}
|
35 |
+
};
|
36 |
+
fetchBranches();
|
37 |
+
}, []);
|
38 |
+
|
39 |
+
const handleShowDeleteModal = (feedId) => {
|
40 |
+
setSelectedStore(feedId);
|
41 |
+
setShowDeleteModal(true);
|
42 |
+
};
|
43 |
+
|
44 |
+
// Đóng modal
|
45 |
+
const handleCloseDeleteModal = () => {
|
46 |
+
setShowDeleteModal(false);
|
47 |
+
setSelectedStore(null);
|
48 |
+
};
|
49 |
+
|
50 |
+
function truncateText(text, maxLength = 120) {
|
51 |
+
if (text.length <= maxLength) {
|
52 |
+
return text;
|
53 |
+
}
|
54 |
+
return text.slice(0, maxLength) + '...Xem thêm';
|
55 |
+
}
|
56 |
+
|
57 |
+
let storesContent;
|
58 |
+
|
59 |
+
if (loading) {
|
60 |
+
storesContent = (<p>Đang tải danh sách chi nhánh...</p>); // Hiển thị thông báo khi đang tải dữ liệu
|
61 |
+
} else {
|
62 |
+
storesContent = (<Col xs={12} md={9}>
|
63 |
+
<Container fluid>
|
64 |
+
<Row xs={1} md={2} xl={3} className="g-4">
|
65 |
+
{Array.from(stores).map((store, idx) => (
|
66 |
+
<Col key={idx}>
|
67 |
+
<StoreItem storeName={store.name}
|
68 |
+
address={truncateText(store.location)}
|
69 |
+
imageSrc={store.image_url}
|
70 |
+
deleteable={true}
|
71 |
+
delButtonCallback={(e) => {
|
72 |
+
e.preventDefault();
|
73 |
+
handleShowDeleteModal(store.id);
|
74 |
+
}}
|
75 |
+
storeHref={`/admin-branchs?id=${store.id}`}
|
76 |
+
>
|
77 |
+
</StoreItem>
|
78 |
+
</Col>
|
79 |
+
))}
|
80 |
+
</Row>
|
81 |
+
</Container>
|
82 |
+
</Col>)
|
83 |
+
}
|
84 |
+
|
85 |
+
const confirmDelete = async () => {
|
86 |
+
if (selectedStore) {
|
87 |
+
axios.delete(process.env.REACT_APP_API_URL + `/branchs/${selectedStore}`)
|
88 |
+
.then((response) => {
|
89 |
+
handleCloseDeleteModal(); // Đóng modal sau khi xóa
|
90 |
+
window.location.reload(); // Load lại trang
|
91 |
+
})
|
92 |
+
.catch((error) => console.error("Xóa bài đăng thất bại:", error))
|
93 |
+
}
|
94 |
+
};
|
95 |
+
|
96 |
+
return (
|
97 |
+
<AdminTemplate content={
|
98 |
+
(<Container fluid className="text-center justify-content-center align-items-center my-5" style={{ maxWidth: "90%" }}>
|
99 |
+
{/* Modal xác nhận xóa */}
|
100 |
+
<Modal show={showDeleteModal} onHide={handleCloseDeleteModal}>
|
101 |
+
<Modal.Header closeButton>
|
102 |
+
<Modal.Title>Xác nhận xóa</Modal.Title>
|
103 |
+
</Modal.Header>
|
104 |
+
<Modal.Body>Bạn có chắc chắn muốn xóa chi nhánh này không?</Modal.Body>
|
105 |
+
<Modal.Footer>
|
106 |
+
<Button variant="secondary" onClick={handleCloseDeleteModal}>
|
107 |
+
Hủy
|
108 |
+
</Button>
|
109 |
+
<Button variant="danger" onClick={confirmDelete}>
|
110 |
+
Xóa
|
111 |
+
</Button>
|
112 |
+
</Modal.Footer>
|
113 |
+
</Modal>
|
114 |
+
|
115 |
+
<h1 className='mb-5'>Danh sách các chi nhánh</h1>
|
116 |
+
<Row className='my-5 justify-content-center align-items-center'>
|
117 |
+
<Col xs={8} md={4} lg={2}>
|
118 |
+
<Button as='a' href={`/admin-branchs`}>
|
119 |
+
Bổ sung chi nhánh mới
|
120 |
+
</Button>
|
121 |
+
</Col>
|
122 |
+
</Row>
|
123 |
+
<Row className="align-items-center">
|
124 |
+
{storesContent}
|
125 |
+
</Row>
|
126 |
+
</Container>)
|
127 |
+
} />
|
128 |
+
);
|
129 |
+
}
|
frontend/src/pages/admin-pages/AdminFeedPage.js
ADDED
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Container, Row, Col, Card, Button, Modal } from "react-bootstrap";
|
2 |
+
import AdminTemplate from "../../templates/AdminTemplate";
|
3 |
+
import axios from 'axios';
|
4 |
+
import { useNavigate } from "react-router-dom";
|
5 |
+
import DataStorage from "../../organisms/DataStorage";
|
6 |
+
import { useEffect, useState } from "react";
|
7 |
+
|
8 |
+
export default function AdminFeedPage() {
|
9 |
+
|
10 |
+
const navigate = useNavigate();
|
11 |
+
|
12 |
+
useEffect(() => {
|
13 |
+
if (!DataStorage.get('isLoggedInAdmin')) {
|
14 |
+
navigate('/admin-login');
|
15 |
+
}
|
16 |
+
}, [navigate]);
|
17 |
+
|
18 |
+
const [feeds, setFeeds] = useState([]);
|
19 |
+
const [loading, setLoading] = useState(true);
|
20 |
+
|
21 |
+
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
22 |
+
const [selectedFeedId, setSelectedFeedId] = useState(null);
|
23 |
+
|
24 |
+
// Mở modal và lưu trữ id của bài đăng cần xóa
|
25 |
+
const handleShowDeleteModal = (feedId) => {
|
26 |
+
setSelectedFeedId(feedId);
|
27 |
+
setShowDeleteModal(true);
|
28 |
+
};
|
29 |
+
|
30 |
+
// Đóng modal
|
31 |
+
const handleCloseDeleteModal = () => {
|
32 |
+
setShowDeleteModal(false);
|
33 |
+
setSelectedFeedId(null);
|
34 |
+
};
|
35 |
+
|
36 |
+
const confirmDelete = async () => {
|
37 |
+
if (selectedFeedId) {
|
38 |
+
axios.delete(process.env.REACT_APP_API_URL + `/feeds/${selectedFeedId}`)
|
39 |
+
.then((response) => {
|
40 |
+
handleCloseDeleteModal(); // Đóng modal sau khi xóa
|
41 |
+
window.location.reload(); // Load lại trang
|
42 |
+
})
|
43 |
+
.catch((error) => console.error("Xóa bài đăng thất bại:", error))
|
44 |
+
}
|
45 |
+
};
|
46 |
+
|
47 |
+
useEffect(() => {
|
48 |
+
axios.get(process.env.REACT_APP_API_URL + '/feeds?limit=100')
|
49 |
+
.then((response) => {
|
50 |
+
setFeeds(response.data.data);
|
51 |
+
setLoading(false);
|
52 |
+
})
|
53 |
+
.catch((error) => {
|
54 |
+
console.log(error);
|
55 |
+
})
|
56 |
+
}, []);
|
57 |
+
|
58 |
+
function truncateText(text, maxLength = 120) {
|
59 |
+
if (text.length <= maxLength) {
|
60 |
+
return text;
|
61 |
+
}
|
62 |
+
return text.slice(0, maxLength) + '...Xem thêm';
|
63 |
+
}
|
64 |
+
|
65 |
+
function formatDate(isoDateString) {
|
66 |
+
const date = new Date(isoDateString);
|
67 |
+
const options = {
|
68 |
+
year: 'numeric',
|
69 |
+
month: 'numeric',
|
70 |
+
day: 'numeric',
|
71 |
+
hour: '2-digit',
|
72 |
+
minute: '2-digit',
|
73 |
+
second: '2-digit'
|
74 |
+
};
|
75 |
+
return date.toLocaleDateString('vi-VN', options);
|
76 |
+
}
|
77 |
+
|
78 |
+
let feedContent;
|
79 |
+
|
80 |
+
if (loading) {
|
81 |
+
feedContent = (<p>Đang tải danh sách bài đăng...</p>);
|
82 |
+
} else {
|
83 |
+
feedContent = (
|
84 |
+
<Container fluid className="d-flex text-center align-items-center justify-content-center">
|
85 |
+
<Row style={{ maxWidth: "90vw" }} className="d-flex align-items-center justify-content-center text-center">
|
86 |
+
|
87 |
+
<Col xs="12" md='8' className="text-center">
|
88 |
+
<h2 className="text-center mb-4">Danh sách các bài đăng trên trang chủ</h2>
|
89 |
+
</Col>
|
90 |
+
|
91 |
+
<Col xs="12" md='4' className="text-center mb-4">
|
92 |
+
<Button className="m-3" as='a' href='/admin-news'>
|
93 |
+
Tạo bài đăng mới
|
94 |
+
</Button>
|
95 |
+
</Col>
|
96 |
+
|
97 |
+
{feeds.map((item, idx) => (
|
98 |
+
<Col xs="12" md="10" lg="8" key={idx} className="m-3 text-center">
|
99 |
+
<Card as='a' href={`/admin-news?id=${item.id}`} className="shadow-sm" style={{ display: 'flex', flexDirection: 'row', height: '200px' }}>
|
100 |
+
<Card.Img
|
101 |
+
variant="left"
|
102 |
+
src={item.image_url}
|
103 |
+
style={{ width: '150px', objectFit: 'cover' }}
|
104 |
+
/>
|
105 |
+
<Card.Body>
|
106 |
+
<Row style={{ height: '100px' }}>
|
107 |
+
<Col xs={12} md={12} className="d-flex align-items-center">
|
108 |
+
<Card.Title>{item.title}</Card.Title>
|
109 |
+
</Col>
|
110 |
+
<Col xs={12} md={12} className="d-flex align-items-center my-3">
|
111 |
+
<Card.Text>{truncateText(item.description)}</Card.Text>
|
112 |
+
</Col>
|
113 |
+
<Col xs={12} md={6} className="d-flex align-items-center">
|
114 |
+
<Card.Text>Tác giả: {item.author_id ? item.author_id : 'Không rõ'}</Card.Text>
|
115 |
+
</Col>
|
116 |
+
<Col xs={12} md={6} className="d-flex align-items-center">
|
117 |
+
<Card.Text>Thời gian tạo: {formatDate(item.create_at)}</Card.Text>
|
118 |
+
</Col>
|
119 |
+
<Col xs={12} className="d-flex justify-content-end">
|
120 |
+
<Button
|
121 |
+
variant="outline-danger"
|
122 |
+
size="sm"
|
123 |
+
onClick={(e) => {
|
124 |
+
e.preventDefault(); // Ngăn link href
|
125 |
+
handleShowDeleteModal(item.id);
|
126 |
+
}}
|
127 |
+
>
|
128 |
+
Xóa
|
129 |
+
</Button>
|
130 |
+
</Col>
|
131 |
+
</Row>
|
132 |
+
</Card.Body>
|
133 |
+
</Card>
|
134 |
+
</Col>
|
135 |
+
))}
|
136 |
+
</Row>
|
137 |
+
</Container>
|
138 |
+
);
|
139 |
+
}
|
140 |
+
|
141 |
+
return (
|
142 |
+
<AdminTemplate content={
|
143 |
+
(
|
144 |
+
<Container className='d-flex text-center align-items-center justify-content-center' style={{ minHeight: '80vh' }}>
|
145 |
+
|
146 |
+
{/* Modal xác nhận xóa */}
|
147 |
+
<Modal show={showDeleteModal} onHide={handleCloseDeleteModal}>
|
148 |
+
<Modal.Header closeButton>
|
149 |
+
<Modal.Title>Xác nhận xóa</Modal.Title>
|
150 |
+
</Modal.Header>
|
151 |
+
<Modal.Body>Bạn có chắc chắn muốn xóa bài đăng này không?</Modal.Body>
|
152 |
+
<Modal.Footer>
|
153 |
+
<Button variant="secondary" onClick={handleCloseDeleteModal}>
|
154 |
+
Hủy
|
155 |
+
</Button>
|
156 |
+
<Button variant="danger" onClick={confirmDelete}>
|
157 |
+
Xóa
|
158 |
+
</Button>
|
159 |
+
</Modal.Footer>
|
160 |
+
</Modal>
|
161 |
+
|
162 |
+
{feedContent}
|
163 |
+
</Container>
|
164 |
+
)
|
165 |
+
} />
|
166 |
+
);
|
167 |
+
}
|
frontend/src/pages/{AdminLoginPage.js → admin-pages/AdminLoginPage.js}
RENAMED
@@ -1,10 +1,10 @@
|
|
1 |
import { useState } from "react";
|
2 |
import { Alert, Container, Form, Row, Col, Button, Card } from "react-bootstrap";
|
3 |
-
import AdminTemplate from "
|
4 |
import { useNavigate } from "react-router-dom";
|
5 |
import axios from 'axios';
|
6 |
-
import jwtDecoder from "
|
7 |
-
import DataStorage from "
|
8 |
|
9 |
export default function AdminLoginPage() {
|
10 |
|
|
|
1 |
import { useState } from "react";
|
2 |
import { Alert, Container, Form, Row, Col, Button, Card } from "react-bootstrap";
|
3 |
+
import AdminTemplate from "../../templates/AdminTemplate";
|
4 |
import { useNavigate } from "react-router-dom";
|
5 |
import axios from 'axios';
|
6 |
+
import jwtDecoder from "../../organisms/jwtDecoder";
|
7 |
+
import DataStorage from "../../organisms/DataStorage";
|
8 |
|
9 |
export default function AdminLoginPage() {
|
10 |
|
frontend/src/pages/admin-pages/AdminMenuEditPage.js
ADDED
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from 'react';
|
2 |
+
import { Modal, Button, Container, Row, Col, Card, Form, Alert, Image } from 'react-bootstrap';
|
3 |
+
import AdminTemplate from "../../templates/AdminTemplate";
|
4 |
+
import DataStorage from '../../organisms/DataStorage';
|
5 |
+
import axios from 'axios';
|
6 |
+
import { useNavigate, useSearchParams } from 'react-router-dom';
|
7 |
+
|
8 |
+
export default function AdminMenuEditPage() {
|
9 |
+
|
10 |
+
const navigate = useNavigate();
|
11 |
+
|
12 |
+
useEffect(() => {
|
13 |
+
if (!DataStorage.get('isLoggedInAdmin')) {
|
14 |
+
navigate('/admin-login');
|
15 |
+
}
|
16 |
+
}, [navigate]);
|
17 |
+
|
18 |
+
const [searchParams] = useSearchParams();
|
19 |
+
const selectedItem = searchParams.get('id') || null;
|
20 |
+
|
21 |
+
const [error, setError] = useState("");
|
22 |
+
// const [loading, setLoading] = useState(true);
|
23 |
+
const [itemDetails, setItemDetails] = useState({
|
24 |
+
id: '',
|
25 |
+
item_name: '',
|
26 |
+
item_type: '1',
|
27 |
+
price: '',
|
28 |
+
description: '',
|
29 |
+
image_url: ''
|
30 |
+
});
|
31 |
+
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
32 |
+
|
33 |
+
const handleShowDeleteModal = () => {
|
34 |
+
setShowDeleteModal(true);
|
35 |
+
}
|
36 |
+
|
37 |
+
const handleCloseDeleteModal = () => {
|
38 |
+
setShowDeleteModal(false);
|
39 |
+
}
|
40 |
+
|
41 |
+
const handleSubmit = async () => {
|
42 |
+
itemDetails.price = Number(itemDetails.price);
|
43 |
+
itemDetails.item_type = Number(itemDetails.item_type);
|
44 |
+
if (selectedItem) {
|
45 |
+
axios.patch(process.env.REACT_APP_API_URL + `/menu-items/${selectedItem}`, itemDetails)
|
46 |
+
.then((response) => {
|
47 |
+
navigate('/admin-menu');
|
48 |
+
})
|
49 |
+
.catch((error) => {
|
50 |
+
setError(JSON.stringify(error));
|
51 |
+
})
|
52 |
+
} else {
|
53 |
+
axios.post(process.env.REACT_APP_API_URL + `/menu-items`, itemDetails)
|
54 |
+
.then((response) => {
|
55 |
+
navigate('/admin-menu');
|
56 |
+
})
|
57 |
+
.catch((error) => {
|
58 |
+
setError(JSON.stringify(error));
|
59 |
+
})
|
60 |
+
}
|
61 |
+
}
|
62 |
+
|
63 |
+
const handleChange = (e) => {
|
64 |
+
const { name, value } = e.target;
|
65 |
+
setItemDetails((prevDetails) => ({
|
66 |
+
...prevDetails,
|
67 |
+
[name]: value, // Sử dụng tên của field để cập nhật động
|
68 |
+
}));
|
69 |
+
};
|
70 |
+
|
71 |
+
const handleAvatarUrlChange = (e) => {
|
72 |
+
const url = e.target.value;
|
73 |
+
setItemDetails((prevData) => ({
|
74 |
+
...prevData,
|
75 |
+
image_url: url // Cập nhật URL ảnh
|
76 |
+
}));
|
77 |
+
console.log(url);
|
78 |
+
};
|
79 |
+
|
80 |
+
const confirmDelete = async () => {
|
81 |
+
if (selectedItem) {
|
82 |
+
axios.delete(process.env.REACT_APP_API_URL + `/menu-items/${selectedItem}`)
|
83 |
+
.then((response) => {
|
84 |
+
handleCloseDeleteModal();
|
85 |
+
navigate('/admin-menu');
|
86 |
+
})
|
87 |
+
.catch((error) => {
|
88 |
+
setError(JSON.stringify(error));
|
89 |
+
})
|
90 |
+
}
|
91 |
+
}
|
92 |
+
|
93 |
+
useEffect(() => {
|
94 |
+
const fetchItemDetail = async () => {
|
95 |
+
if (selectedItem) {
|
96 |
+
try {
|
97 |
+
const response = await axios.get(process.env.REACT_APP_API_URL + `/menu-items/${selectedItem}`);
|
98 |
+
setItemDetails(response.data);
|
99 |
+
// setLoading(false);
|
100 |
+
} catch (error) {
|
101 |
+
setError(JSON.stringify(error));
|
102 |
+
}
|
103 |
+
} else {
|
104 |
+
setItemDetails(
|
105 |
+
{
|
106 |
+
id: '',
|
107 |
+
item_name: '',
|
108 |
+
item_type: '1',
|
109 |
+
price: '',
|
110 |
+
description: '',
|
111 |
+
image_url: ''
|
112 |
+
}
|
113 |
+
)
|
114 |
+
}
|
115 |
+
}
|
116 |
+
fetchItemDetail();
|
117 |
+
}, [selectedItem])
|
118 |
+
|
119 |
+
return (
|
120 |
+
<AdminTemplate content={(
|
121 |
+
<Container className='text-center align-items-center justify-content-center' style={{ minHeight: '80vh' }}>
|
122 |
+
{/* Modal xác nhận xóa */}
|
123 |
+
<Modal show={showDeleteModal} onHide={handleCloseDeleteModal}>
|
124 |
+
<Modal.Header closeButton>
|
125 |
+
<Modal.Title>Xác nhận xóa</Modal.Title>
|
126 |
+
</Modal.Header>
|
127 |
+
<Modal.Body>Bạn có chắc chắn muốn xóa món này khỏi menu không?</Modal.Body>
|
128 |
+
<Modal.Footer>
|
129 |
+
<Button variant="secondary" onClick={handleCloseDeleteModal}>
|
130 |
+
Hủy
|
131 |
+
</Button>
|
132 |
+
<Button variant="danger" onClick={confirmDelete}>
|
133 |
+
Xóa
|
134 |
+
</Button>
|
135 |
+
</Modal.Footer>
|
136 |
+
</Modal>
|
137 |
+
|
138 |
+
<Row className="align-items-center">
|
139 |
+
{/* <Col xs={1} md={2}></Col> */}
|
140 |
+
<Col>
|
141 |
+
<Card style={{ width: '100%' }} className='justify-content-center'>
|
142 |
+
<Card.Header>
|
143 |
+
<Card.Title className='mt-1 text-center'>{selectedItem ? 'Chỉnh sửa thông tin chi nhánh' : 'Tạo chi nhánh'}</Card.Title>
|
144 |
+
</Card.Header>
|
145 |
+
<Card.Body>
|
146 |
+
|
147 |
+
<Form>
|
148 |
+
{error && <Alert variant="danger">{error}</Alert>}
|
149 |
+
<Row className="mb-3">
|
150 |
+
<Col xs={12} className="text-center mb-3">
|
151 |
+
{/* Hiển thị ảnh từ URL */}
|
152 |
+
<Image
|
153 |
+
src={itemDetails.image_url || ''} // Hiển thị ảnh đại diện từ URL
|
154 |
+
alt="Ảnh món"
|
155 |
+
style={{ width: "100%", maxWidth: "100%", height: "auto" }}
|
156 |
+
className="mb-3"
|
157 |
+
/>
|
158 |
+
<Form.Group controlId="formImageUrl">
|
159 |
+
<Form.Label>URL ảnh món</Form.Label>
|
160 |
+
<Form.Control
|
161 |
+
type="text"
|
162 |
+
placeholder="Nhập URL ảnh"
|
163 |
+
value={itemDetails.image_url || ''}
|
164 |
+
onChange={handleAvatarUrlChange} // Gọi hàm khi URL thay đổi
|
165 |
+
/>
|
166 |
+
</Form.Group>
|
167 |
+
</Col>
|
168 |
+
<Col xs={12} className="mb-3">
|
169 |
+
<Form.Group controlId="formName">
|
170 |
+
<Form.Label>Tên món</Form.Label>
|
171 |
+
<Form.Control
|
172 |
+
type="text"
|
173 |
+
name="item_name"
|
174 |
+
placeholder={selectedItem ? 'Đang tải dữ liệu...' : 'Tên món'}
|
175 |
+
value={itemDetails.item_name || ''}
|
176 |
+
onChange={handleChange}
|
177 |
+
/>
|
178 |
+
</Form.Group>
|
179 |
+
</Col>
|
180 |
+
<Col xs={12} className="mb-3">
|
181 |
+
<Form.Group controlId="formName">
|
182 |
+
<Form.Label>Id món</Form.Label>
|
183 |
+
<Form.Control
|
184 |
+
type="text"
|
185 |
+
name="id"
|
186 |
+
disabled={selectedItem ? true : false}
|
187 |
+
placeholder={selectedItem ? 'Đang tải dữ liệu...' : 'Mã id món'}
|
188 |
+
value={itemDetails.id || ''}
|
189 |
+
onChange={handleChange}
|
190 |
+
/>
|
191 |
+
</Form.Group>
|
192 |
+
</Col>
|
193 |
+
<Col xs={12} className="mb-3">
|
194 |
+
<Form.Group controlId="formText" className="mb-3">
|
195 |
+
<Form.Label>Mô tả món</Form.Label>
|
196 |
+
<Form.Control
|
197 |
+
as="textarea"
|
198 |
+
name="description"
|
199 |
+
placeholder={selectedItem ? 'Đang tải dữ liệu...' : 'Mô tả món'}
|
200 |
+
rows={3}
|
201 |
+
value={itemDetails.description || ''}
|
202 |
+
onChange={handleChange}
|
203 |
+
/>
|
204 |
+
</Form.Group>
|
205 |
+
</Col>
|
206 |
+
|
207 |
+
<Col xs={12} className='mb-3'>
|
208 |
+
<Form.Group controlId="formType" className="mb-3">
|
209 |
+
<Form.Label>Chọn danh mục món ăn</Form.Label>
|
210 |
+
<Form.Select name='item_type' value={itemDetails.item_type || '1'} onChange={handleChange}>
|
211 |
+
<option value="1">Món chính</option>
|
212 |
+
<option value="2">Tráng miệng</option>
|
213 |
+
<option value="3">Đồ uống</option>
|
214 |
+
</Form.Select>
|
215 |
+
</Form.Group>
|
216 |
+
</Col>
|
217 |
+
|
218 |
+
<Col xs={12} className='mb-5'>
|
219 |
+
<Form.Group>
|
220 |
+
<Form.Label>Nhập giá tiền</Form.Label>
|
221 |
+
<Form.Control
|
222 |
+
type='text'
|
223 |
+
name='price'
|
224 |
+
placeholder={selectedItem ? 'Đang tải dữ liệu...' : 'Giá món'}
|
225 |
+
value={itemDetails.price || ''}
|
226 |
+
onChange={handleChange}
|
227 |
+
/>
|
228 |
+
</Form.Group>
|
229 |
+
</Col>
|
230 |
+
|
231 |
+
<Col xs={12} className='mb-3'>
|
232 |
+
<Button variant="primary" type="button" className='mx-5' onClick={handleSubmit}>
|
233 |
+
{selectedItem ? 'Cập nhật' : 'Tạo mới'}
|
234 |
+
</Button>
|
235 |
+
{selectedItem ?
|
236 |
+
(<Button variant="danger" type="button" className='mx-5' onClick={handleShowDeleteModal}>
|
237 |
+
Xóa
|
238 |
+
</Button>) : (<></>)
|
239 |
+
}
|
240 |
+
</Col>
|
241 |
+
</Row>
|
242 |
+
</Form>
|
243 |
+
</Card.Body>
|
244 |
+
</Card>
|
245 |
+
|
246 |
+
</Col>
|
247 |
+
{/* <Col xs={1} md={2}></Col> */}
|
248 |
+
</Row>
|
249 |
+
</Container>
|
250 |
+
)} />
|
251 |
+
)
|
252 |
+
}
|
frontend/src/pages/admin-pages/AdminMenuPage.js
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from 'react';
|
2 |
+
import { Button, Container, Row, Col, Tab, Tabs, Alert } from 'react-bootstrap';
|
3 |
+
import AdminTemplate from "../../templates/AdminTemplate";
|
4 |
+
import DataStorage from '../../organisms/DataStorage';
|
5 |
+
import axios from 'axios';
|
6 |
+
import MenuItem from '../../molecules/MenuItem';
|
7 |
+
import { useNavigate } from 'react-router-dom';
|
8 |
+
|
9 |
+
const categoryMapper = [
|
10 |
+
{ itemType: 1, category: 'Món chính' },
|
11 |
+
{ itemType: 2, category: 'Đồ uống' },
|
12 |
+
{ itemType: 3, category: 'Tráng miệng' },
|
13 |
+
];
|
14 |
+
|
15 |
+
export default function AdminMenuPage() {
|
16 |
+
|
17 |
+
const navigate = useNavigate();
|
18 |
+
|
19 |
+
useEffect(() => {
|
20 |
+
if (!DataStorage.get('isLoggedInAdmin')) {
|
21 |
+
navigate('/admin-login');
|
22 |
+
}
|
23 |
+
}, [navigate]);
|
24 |
+
|
25 |
+
const [error, setError] = useState("");
|
26 |
+
const [loading, setLoading] = useState(true);
|
27 |
+
const [menuItems, setMenuItems] = useState({});
|
28 |
+
const [key, setKey] = useState(0);
|
29 |
+
|
30 |
+
function organizeMenuItemsByType(menuData) {
|
31 |
+
// Lấy ra tất cả menu_item từ array ban đầu
|
32 |
+
const menuItems = menuData.map(item => item);
|
33 |
+
|
34 |
+
// Tạo một object để nhóm menu_item theo item_type, khởi tạo các mảng rỗng cho mỗi item_type từ categoryMapper
|
35 |
+
const organizedItems = categoryMapper.reduce((acc, { itemType }) => {
|
36 |
+
acc[itemType] = [];
|
37 |
+
return acc;
|
38 |
+
}, {});
|
39 |
+
|
40 |
+
// Thêm menu_item vào organizedItems theo item_type
|
41 |
+
menuItems.forEach(menuItem => {
|
42 |
+
const type = menuItem.item_type;
|
43 |
+
|
44 |
+
// Đưa menu_item vào array của item_type tương ứng
|
45 |
+
if (organizedItems[type]) {
|
46 |
+
organizedItems[type].push(menuItem);
|
47 |
+
}
|
48 |
+
});
|
49 |
+
|
50 |
+
return organizedItems;
|
51 |
+
}
|
52 |
+
|
53 |
+
useEffect(() => {
|
54 |
+
const fetchMenuItems = async () => {
|
55 |
+
try {
|
56 |
+
let menuItemsByType = {};
|
57 |
+
// Gọi API để lấy món theo itemType
|
58 |
+
const response = await axios.get(process.env.REACT_APP_API_URL + `/menu-items?limit=100`);
|
59 |
+
// Lưu danh sách món theo itemType
|
60 |
+
console.log('Response', response.data.data);
|
61 |
+
menuItemsByType = organizeMenuItemsByType(response.data.data);
|
62 |
+
|
63 |
+
console.log(menuItemsByType);
|
64 |
+
setMenuItems(menuItemsByType);
|
65 |
+
setLoading(false);
|
66 |
+
} catch (error) {
|
67 |
+
setError(JSON.stringify(error));
|
68 |
+
setLoading(false);
|
69 |
+
}
|
70 |
+
}
|
71 |
+
fetchMenuItems()
|
72 |
+
}, [])
|
73 |
+
|
74 |
+
|
75 |
+
let menuContent;
|
76 |
+
|
77 |
+
if (loading) {
|
78 |
+
menuContent = (<p>Đang tải thực đơn...</p>);
|
79 |
+
} else {
|
80 |
+
menuContent = (<Tabs
|
81 |
+
id="controlled-tab-example"
|
82 |
+
activeKey={key}
|
83 |
+
onSelect={(k) => setKey(k)}
|
84 |
+
className="mb-3 custom-tab"
|
85 |
+
>
|
86 |
+
{categoryMapper.map((category, index) => (
|
87 |
+
<Tab eventKey={index} title={category.category}>
|
88 |
+
<Container fluid className='my-5'>
|
89 |
+
<Row md={3} className="g-4">
|
90 |
+
{menuItems[category.itemType]?.map((item, idx) => (
|
91 |
+
<Col key={item.id}>
|
92 |
+
<div onClick={() => navigate(`/admin-menu-edit?id=${item.id}`)}>
|
93 |
+
<MenuItem
|
94 |
+
dishName={item.item_name}
|
95 |
+
description={item.description}
|
96 |
+
imageSrc={item.image_url}
|
97 |
+
/>
|
98 |
+
</div>
|
99 |
+
</Col>
|
100 |
+
))}
|
101 |
+
</Row>
|
102 |
+
</Container>
|
103 |
+
</Tab>
|
104 |
+
))}
|
105 |
+
</Tabs>);
|
106 |
+
}
|
107 |
+
|
108 |
+
return (
|
109 |
+
<AdminTemplate content={
|
110 |
+
(
|
111 |
+
<Container className='text-center align-items-center justify-content-center' style={{ minHeight: '80vh' }}>
|
112 |
+
|
113 |
+
{error && <Alert variant="danger">{error}</Alert>}
|
114 |
+
|
115 |
+
<h1 className='mb-5'>Menu</h1>
|
116 |
+
<Row className='my-5 justify-content-center align-items-center'>
|
117 |
+
<Col xs={8} md={4} lg={2}>
|
118 |
+
<Button onClick={() => navigate(`/admin-menu-edit`)}>
|
119 |
+
Bổ sung món mới
|
120 |
+
</Button>
|
121 |
+
</Col>
|
122 |
+
</Row>
|
123 |
+
<Row className="align-items-center">
|
124 |
+
{menuContent}
|
125 |
+
</Row>
|
126 |
+
</Container>
|
127 |
+
)
|
128 |
+
} />
|
129 |
+
);
|
130 |
+
}
|
frontend/src/pages/admin-pages/AdminNewsEditPage.js
ADDED
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Container, Row, Col, Card, Form, Image, Alert, Button } from "react-bootstrap";
|
2 |
+
import AdminTemplate from "../../templates/AdminTemplate";
|
3 |
+
import { useSearchParams } from 'react-router-dom';
|
4 |
+
import axios from "axios";
|
5 |
+
import DataStorage from "../../organisms/DataStorage";
|
6 |
+
import { useEffect, useState } from "react";
|
7 |
+
import { useNavigate } from "react-router-dom";
|
8 |
+
|
9 |
+
export default function AdminNewsEditPage() {
|
10 |
+
|
11 |
+
const [error, setError] = useState("");
|
12 |
+
const [searchParams] = useSearchParams();
|
13 |
+
const newsId = Number(searchParams.get('id')) || null;
|
14 |
+
|
15 |
+
const [feedDetail, setFeedItem] = useState({});
|
16 |
+
const [initialUrl, setInitialUrl] = useState("");
|
17 |
+
const [initialTitle, setInitialTitle] = useState("");
|
18 |
+
const [initialDesc, setInitialDesc] = useState("");
|
19 |
+
// const [loading, setLoading] = useState(true);
|
20 |
+
const [isChanged, setChanged] = useState(false);
|
21 |
+
|
22 |
+
const navigate = useNavigate();
|
23 |
+
|
24 |
+
useEffect(() => {
|
25 |
+
if (!DataStorage.get('isLoggedInAdmin')) {
|
26 |
+
navigate('/admin-login');
|
27 |
+
}
|
28 |
+
}, [navigate]);
|
29 |
+
|
30 |
+
const checkChange = () => {
|
31 |
+
if (feedDetail.title.trim() !== initialTitle.trim()
|
32 |
+
|| feedDetail.description.trim() !== initialDesc.trim()
|
33 |
+
|| feedDetail.image_url.trim() !== initialUrl.trim()) {
|
34 |
+
setChanged(true);
|
35 |
+
} else {
|
36 |
+
setChanged(false);
|
37 |
+
}
|
38 |
+
}
|
39 |
+
|
40 |
+
const handleChange = (e) => {
|
41 |
+
const { name, value } = e.target;
|
42 |
+
setFeedItem({ ...feedDetail, [name]: value });
|
43 |
+
checkChange();
|
44 |
+
};
|
45 |
+
|
46 |
+
const handleAvatarUrlChange = (e) => {
|
47 |
+
const url = e.target.value;
|
48 |
+
setFeedItem((prevData) => ({
|
49 |
+
...prevData,
|
50 |
+
image_url: url // Cập nhật URL ảnh
|
51 |
+
}));
|
52 |
+
setChanged(true);
|
53 |
+
};
|
54 |
+
|
55 |
+
useEffect(() => {
|
56 |
+
if (newsId) {
|
57 |
+
axios.get(process.env.REACT_APP_API_URL + `/feeds/${newsId}`)
|
58 |
+
.then((response) => {
|
59 |
+
setFeedItem(response.data);
|
60 |
+
// setLoading(false);
|
61 |
+
setInitialDesc(response.data.description);
|
62 |
+
setInitialTitle(response.data.title);
|
63 |
+
setInitialUrl(response.data.image_url);
|
64 |
+
})
|
65 |
+
.catch((error) => {
|
66 |
+
setError(JSON.stringify(error));
|
67 |
+
})
|
68 |
+
}
|
69 |
+
}, [newsId]);
|
70 |
+
|
71 |
+
const handleSubmit = () => {
|
72 |
+
const submit_data = {
|
73 |
+
'title': feedDetail.title,
|
74 |
+
'description': feedDetail.description,
|
75 |
+
'image_url': feedDetail.image_url
|
76 |
+
}
|
77 |
+
if (newsId) {
|
78 |
+
axios.patch(process.env.REACT_APP_API_URL + `/feeds/${newsId}`, submit_data)
|
79 |
+
.then((response) => {
|
80 |
+
// setFeedItem(response.data);
|
81 |
+
// // setLoading(false);
|
82 |
+
// setInitialDesc(response.data.description);
|
83 |
+
// setInitialTitle(response.data.title);
|
84 |
+
// setChanged(false);
|
85 |
+
navigate('/admin-feed');
|
86 |
+
// window.location.reload();
|
87 |
+
})
|
88 |
+
.catch((error) => {
|
89 |
+
setError(JSON.stringify(error));
|
90 |
+
})
|
91 |
+
} else {
|
92 |
+
axios.post(process.env.REACT_APP_API_URL + `/feeds`, submit_data)
|
93 |
+
.then((response) => {
|
94 |
+
navigate('/admin-feed');
|
95 |
+
})
|
96 |
+
.catch((error) => {
|
97 |
+
setError(JSON.stringify(error));
|
98 |
+
})
|
99 |
+
}
|
100 |
+
}
|
101 |
+
|
102 |
+
return (
|
103 |
+
<AdminTemplate content={
|
104 |
+
(
|
105 |
+
<Container fluid className="d-flex align-items-center justify-content-center mt-5">
|
106 |
+
<Row className="align-items-center">
|
107 |
+
{/* <Col xs={1} md={2}></Col> */}
|
108 |
+
<Col>
|
109 |
+
<Card style={{ width: '100%' }} className='justify-content-center'>
|
110 |
+
<Card.Header>
|
111 |
+
<Card.Title className='mt-1 text-center'>{newsId ? 'Chỉnh sửa bài đăng' : 'Tạo bài đăng'}</Card.Title>
|
112 |
+
</Card.Header>
|
113 |
+
<Card.Body>
|
114 |
+
|
115 |
+
<Form>
|
116 |
+
{error && <Alert variant="danger">{error}</Alert>}
|
117 |
+
<Row className="mb-3">
|
118 |
+
<Col xs={12} className="text-center mb-3">
|
119 |
+
{/* Hiển thị ảnh từ URL */}
|
120 |
+
<Image
|
121 |
+
src={feedDetail.image_url} // Hiển thị ảnh đại diện từ URL
|
122 |
+
alt="Ảnh bài đăng"
|
123 |
+
width="auto"
|
124 |
+
height={400}
|
125 |
+
className="mb-3"
|
126 |
+
/>
|
127 |
+
<Form.Group controlId="formImageUrl">
|
128 |
+
<Form.Label>URL ảnh bài đăng</Form.Label>
|
129 |
+
<Form.Control
|
130 |
+
type="text"
|
131 |
+
placeholder="Nhập URL ảnh"
|
132 |
+
value={feedDetail.image_url || ''}
|
133 |
+
onChange={handleAvatarUrlChange} // Gọi hàm khi URL thay đổi
|
134 |
+
/>
|
135 |
+
</Form.Group>
|
136 |
+
</Col>
|
137 |
+
<Col xs={12} className="mb-3">
|
138 |
+
<Form.Group controlId="formTitle">
|
139 |
+
<Form.Label>Tiêu đề bài đăng</Form.Label>
|
140 |
+
<Form.Control
|
141 |
+
type="text"
|
142 |
+
name="title"
|
143 |
+
placeholder={newsId ? 'Đang tải dữ liệu...' : 'Tiêu đề'}
|
144 |
+
value={feedDetail.title || ''}
|
145 |
+
onChange={handleChange}
|
146 |
+
/>
|
147 |
+
</Form.Group>
|
148 |
+
</Col>
|
149 |
+
</Row>
|
150 |
+
|
151 |
+
<Form.Group controlId="formText" className="mb-3">
|
152 |
+
<Form.Label>Nội dung</Form.Label>
|
153 |
+
<Form.Control
|
154 |
+
as="textarea"
|
155 |
+
name="description"
|
156 |
+
placeholder={newsId ? 'Đang tải dữ liệu...' : 'Nội dung'}
|
157 |
+
rows={10}
|
158 |
+
value={feedDetail.description || ''}
|
159 |
+
onChange={handleChange}
|
160 |
+
/>
|
161 |
+
</Form.Group>
|
162 |
+
|
163 |
+
<Button variant="primary" type="button" disabled={!isChanged} onClick={handleSubmit}>
|
164 |
+
{newsId ? 'Cập nhật' : 'Tạo mới'}
|
165 |
+
</Button>
|
166 |
+
</Form>
|
167 |
+
</Card.Body>
|
168 |
+
</Card>
|
169 |
+
|
170 |
+
</Col>
|
171 |
+
{/* <Col xs={1} md={2}></Col> */}
|
172 |
+
</Row>
|
173 |
+
</Container>
|
174 |
+
)
|
175 |
+
} />
|
176 |
+
)
|
177 |
+
}
|
frontend/src/pages/{AdminOrderPage.js → admin-pages/AdminOrderPage.js}
RENAMED
@@ -1,5 +1,5 @@
|
|
1 |
import { Container, Row, Col } from "react-bootstrap";
|
2 |
-
import AdminTemplate from "
|
3 |
|
4 |
export default function AdminOrderPage() {
|
5 |
return (
|
|
|
1 |
import { Container, Row, Col } from "react-bootstrap";
|
2 |
+
import AdminTemplate from "../../templates/AdminTemplate";
|
3 |
|
4 |
export default function AdminOrderPage() {
|
5 |
return (
|
frontend/src/pages/{AdminSchedulePage.js → admin-pages/AdminSchedulePage.js}
RENAMED
@@ -1,5 +1,5 @@
|
|
1 |
import { Container, Row, Col } from "react-bootstrap";
|
2 |
-
import AdminTemplate from "
|
3 |
|
4 |
export default function AdminSchedulePage() {
|
5 |
return (
|
|
|
1 |
import { Container, Row, Col } from "react-bootstrap";
|
2 |
+
import AdminTemplate from "../../templates/AdminTemplate";
|
3 |
|
4 |
export default function AdminSchedulePage() {
|
5 |
return (
|
frontend/src/pages/{AdminStaffPage.js → admin-pages/AdminStaffPage.js}
RENAMED
@@ -1,5 +1,5 @@
|
|
1 |
import { Container, Row, Col } from "react-bootstrap";
|
2 |
-
import AdminTemplate from "
|
3 |
|
4 |
export default function AdminStaffPage() {
|
5 |
return (
|
|
|
1 |
import { Container, Row, Col } from "react-bootstrap";
|
2 |
+
import AdminTemplate from "../../templates/AdminTemplate";
|
3 |
|
4 |
export default function AdminStaffPage() {
|
5 |
return (
|
frontend/src/pages/{AdminSummaryPage.js → admin-pages/AdminSummaryPage.js}
RENAMED
@@ -1,8 +1,10 @@
|
|
1 |
import { Container, Row, Col } from "react-bootstrap";
|
2 |
-
import AdminTemplate from "
|
3 |
import { useNavigate } from "react-router-dom";
|
4 |
import { useEffect } from "react";
|
5 |
-
import DataStorage from "
|
|
|
|
|
6 |
|
7 |
export default function AdminSummaryPage() {
|
8 |
|
@@ -18,13 +20,16 @@ export default function AdminSummaryPage() {
|
|
18 |
<AdminTemplate content={
|
19 |
(
|
20 |
<Container className='d-flex text-center align-items-center justify-content-center' style={{ minHeight: '80vh' }}>
|
21 |
-
<Row>
|
22 |
<Col xs={12}>
|
23 |
<h1>This is a demo summary page</h1>
|
24 |
</Col>
|
25 |
<Col xs={12}>
|
26 |
<h3>In the future, we hope to connect to PowerBI API and show the dashboard</h3>
|
27 |
</Col>
|
|
|
|
|
|
|
28 |
</Row>
|
29 |
</Container>
|
30 |
)
|
|
|
1 |
import { Container, Row, Col } from "react-bootstrap";
|
2 |
+
import AdminTemplate from "../../templates/AdminTemplate";
|
3 |
import { useNavigate } from "react-router-dom";
|
4 |
import { useEffect } from "react";
|
5 |
+
import DataStorage from "../../organisms/DataStorage";
|
6 |
+
|
7 |
+
import SummaryReport from "../../organisms/SummaryReport";
|
8 |
|
9 |
export default function AdminSummaryPage() {
|
10 |
|
|
|
20 |
<AdminTemplate content={
|
21 |
(
|
22 |
<Container className='d-flex text-center align-items-center justify-content-center' style={{ minHeight: '80vh' }}>
|
23 |
+
<Row className="align-items-center">
|
24 |
<Col xs={12}>
|
25 |
<h1>This is a demo summary page</h1>
|
26 |
</Col>
|
27 |
<Col xs={12}>
|
28 |
<h3>In the future, we hope to connect to PowerBI API and show the dashboard</h3>
|
29 |
</Col>
|
30 |
+
<Col xs={12} className='d-flex flex-column align-items-center'>
|
31 |
+
<SummaryReport/>
|
32 |
+
</Col>
|
33 |
</Row>
|
34 |
</Container>
|
35 |
)
|
frontend/src/pages/{AdminUserInfoPage.js → admin-pages/AdminUserInfoPage.js}
RENAMED
@@ -1,9 +1,9 @@
|
|
1 |
-
import AdminTemplate from "
|
2 |
import { Container, Form, Row, Col, Card, Alert, Button, Image } from "react-bootstrap";
|
3 |
import React, { useState, useEffect } from "react";
|
4 |
import validator from "validator";
|
5 |
import axios from "axios";
|
6 |
-
import DataStorage from "
|
7 |
|
8 |
export default function AdminUserInfoPage() {
|
9 |
|
|
|
1 |
+
import AdminTemplate from "../../templates/AdminTemplate";
|
2 |
import { Container, Form, Row, Col, Card, Alert, Button, Image } from "react-bootstrap";
|
3 |
import React, { useState, useEffect } from "react";
|
4 |
import validator from "validator";
|
5 |
import axios from "axios";
|
6 |
+
import DataStorage from "../../organisms/DataStorage";
|
7 |
|
8 |
export default function AdminUserInfoPage() {
|
9 |
|
frontend/src/pages/user-pages/CartPage.js
ADDED
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import BasicTemplate from "../../templates/BasicTemplate";
|
2 |
+
import { Container, Row, Col, Card, Button, Modal, Form } from "react-bootstrap";
|
3 |
+
import { useState, useEffect } from "react";
|
4 |
+
import CacheStorage from "../../organisms/CacheStorage";
|
5 |
+
import DataStorage from "../../organisms/DataStorage";
|
6 |
+
import axios from "axios";
|
7 |
+
|
8 |
+
export default function CartPage() {
|
9 |
+
|
10 |
+
const [cartItems, setCartItems] = useState([]);
|
11 |
+
const [show, setShow] = useState(false);
|
12 |
+
|
13 |
+
const [branches, setBranches] = useState([]);
|
14 |
+
const defaultSelectedStore = CacheStorage.get('selectedStore') || "";
|
15 |
+
const [selectedStore, setSelectedStore] = useState(defaultSelectedStore);
|
16 |
+
const [loadingBranches, setLoadingBranches] = useState(true);
|
17 |
+
const [address, setAddress] = useState('');
|
18 |
+
// const [loading, setLoading] = useState(true);
|
19 |
+
|
20 |
+
useEffect(() => {
|
21 |
+
const fetchBranches = async () => {
|
22 |
+
try {
|
23 |
+
const response = await axios.get(process.env.REACT_APP_API_URL + '/branchs'); // Thay 'API_ENDPOINT' bằng URL của API
|
24 |
+
setBranches(response.data); // Lưu dữ liệu vào state
|
25 |
+
setLoadingBranches(false); // Đặt loading thành false khi hoàn tất
|
26 |
+
CacheStorage.set('stores', JSON.stringify(Object(response.data)));
|
27 |
+
} catch (error) {
|
28 |
+
console.error('Error fetching branches:', error);
|
29 |
+
setLoadingBranches(false); // Đặt loading thành false nếu lỗi
|
30 |
+
}
|
31 |
+
};
|
32 |
+
if (CacheStorage.get('stores')) {
|
33 |
+
setBranches(JSON.parse(CacheStorage.get('stores')));
|
34 |
+
setLoadingBranches(false);
|
35 |
+
} else {
|
36 |
+
fetchBranches();
|
37 |
+
}
|
38 |
+
}, [])
|
39 |
+
|
40 |
+
const handleSelectStore = (e) => {
|
41 |
+
setSelectedStore(e.target.value);
|
42 |
+
CacheStorage.set('selectedStore', e.target.value);
|
43 |
+
// setLoading(true);
|
44 |
+
};
|
45 |
+
|
46 |
+
let selectboxContent;
|
47 |
+
|
48 |
+
if (loadingBranches) {
|
49 |
+
selectboxContent = (<p>Đang tải dữ liệu...</p>)
|
50 |
+
} else {
|
51 |
+
selectboxContent = (<Form>
|
52 |
+
<Form.Group controlId="branchSelect">
|
53 |
+
<Form.Label>Chọn chi nhánh:</Form.Label>
|
54 |
+
<Form.Control as="select" onChange={handleSelectStore} value={selectedStore}>
|
55 |
+
<option value="">-- Chọn chi nhánh --</option>
|
56 |
+
{branches.map((store) => (
|
57 |
+
<option key={store.id} value={store.id}>
|
58 |
+
{store.name + ' - ' + store.location}
|
59 |
+
</option>
|
60 |
+
))}
|
61 |
+
</Form.Control>
|
62 |
+
</Form.Group>
|
63 |
+
</Form>);
|
64 |
+
}
|
65 |
+
|
66 |
+
useEffect(() => {
|
67 |
+
// Lấy giỏ hàng từ sessionStorage
|
68 |
+
const cart = JSON.parse(DataStorage.get('cart')) || {};
|
69 |
+
|
70 |
+
// Chuyển cart thành mảng chứa các món có số lượng > 0
|
71 |
+
const items = Object.entries(cart).map(([id, item]) => ({
|
72 |
+
id: id,
|
73 |
+
name: item.name,
|
74 |
+
amount: item.amount,
|
75 |
+
imageSrc: item.imageSrc,
|
76 |
+
price: item.price
|
77 |
+
}));
|
78 |
+
|
79 |
+
console.log(items);
|
80 |
+
setCartItems(items);
|
81 |
+
}, []);
|
82 |
+
|
83 |
+
const handleShow = () => {
|
84 |
+
setShow(true);
|
85 |
+
}
|
86 |
+
|
87 |
+
const handleClose = () => {
|
88 |
+
setShow(false);
|
89 |
+
}
|
90 |
+
|
91 |
+
const handleSubmit = () => {
|
92 |
+
// tạo order, gửi order && gửi lấy urlpayment
|
93 |
+
|
94 |
+
if (selectedStore === '') {
|
95 |
+
setShow(false);
|
96 |
+
} else {
|
97 |
+
|
98 |
+
// try {
|
99 |
+
// let orderData;
|
100 |
+
|
101 |
+
// orderData.order_type = 2;
|
102 |
+
// orderData.order_items = cartItems.map((item) => { return { menu_id: item.id, quantity: item.amount } })
|
103 |
+
// const orderResponse = axios.post(process.env.REACT_APP_API_URL + `/branches/${selectedStore}/orders`, {
|
104 |
+
// headers: {
|
105 |
+
// Authorization: `Bearer ${DataStorage.get('accessToken')}`,
|
106 |
+
// },
|
107 |
+
// })
|
108 |
+
|
109 |
+
// let paymentData;
|
110 |
+
// paymentData.orderType='other';
|
111 |
+
// paymentData.orderDescription=''
|
112 |
+
// } catch (error) {
|
113 |
+
// console.log('error');
|
114 |
+
// }
|
115 |
+
|
116 |
+
setShow(false);
|
117 |
+
}
|
118 |
+
}
|
119 |
+
|
120 |
+
return (
|
121 |
+
<BasicTemplate content={
|
122 |
+
(
|
123 |
+
<Container className="d-flex align-items-center justify-content-center my-5" style={{ minHeight: '70vh' }}>
|
124 |
+
<Modal show={show} onHide={handleClose} className="text-center">
|
125 |
+
<Modal.Header closeButton className="text-center">
|
126 |
+
<Modal.Title >Xác nhận thanh toán</Modal.Title>
|
127 |
+
</Modal.Header>
|
128 |
+
<Modal.Body>
|
129 |
+
{
|
130 |
+
(selectedStore !== '' && address !== '' ?
|
131 |
+
`Bạn sắp thanh toán ${Object.values(cartItems).reduce((total, item) => { return total + item.price * item.amount; }, 0)} VND qua VNPAY.
|
132 |
+
Vui lòng xác nhận để chuyển tiếp đến trang thanh toán.` : `Hãy đảm bảo bạn chọn địa điểm giao hàng, và chi nhánh đặt món`
|
133 |
+
)
|
134 |
+
}
|
135 |
+
</Modal.Body>
|
136 |
+
<Modal.Footer>
|
137 |
+
<Button variant="primary" onClick={handleSubmit}>
|
138 |
+
Xác nhận
|
139 |
+
</Button>
|
140 |
+
<Button variant='outline-primary' onClick={handleClose}>
|
141 |
+
Quay lại
|
142 |
+
</Button>
|
143 |
+
</Modal.Footer>
|
144 |
+
</Modal>
|
145 |
+
|
146 |
+
{cartItems.length > 0 ? (
|
147 |
+
<Container fluid className="d-flex text-center align-items-center justify-content-center">
|
148 |
+
<Row style={{ maxWidth: "90vw" }} className="justify-content-center">
|
149 |
+
<Col xs="12" md="8" lg="8" className="my-5 text-center">
|
150 |
+
{selectboxContent}
|
151 |
+
</Col>
|
152 |
+
<Col xs="12" md="8" className="my-5 text-center">
|
153 |
+
<Form>
|
154 |
+
<Form.Group>
|
155 |
+
<Form.Label>Chọn địa chỉ giao hàng</Form.Label>
|
156 |
+
<Form.Control
|
157 |
+
type="text"
|
158 |
+
name="address"
|
159 |
+
placeholder='Địa chỉ giao hàng'
|
160 |
+
value={address || ''}
|
161 |
+
onChange={(e) => setAddress(e.target.value)}
|
162 |
+
/>
|
163 |
+
</Form.Group>
|
164 |
+
</Form>
|
165 |
+
</Col>
|
166 |
+
<Col xs="12" className="text-center">
|
167 |
+
<h2 className="text-center mb-4">Giỏ hàng của bạn</h2>
|
168 |
+
</Col>
|
169 |
+
|
170 |
+
{cartItems.map((item, idx) => (
|
171 |
+
<Col xs="12" md="8" lg="8" key={idx} className="m-3 text-center">
|
172 |
+
<Card className="shadow-sm" style={{ display: 'flex', flexDirection: 'row' }}>
|
173 |
+
<Card.Img
|
174 |
+
variant="left"
|
175 |
+
src={item.imageSrc}
|
176 |
+
style={{ width: '150px', objectFit: 'cover' }}
|
177 |
+
/>
|
178 |
+
<Card.Body>
|
179 |
+
<Row xs={4} className="text-center" style={{ height: '100px' }}>
|
180 |
+
<Col className="d-flex align-items-center justify-content-center">
|
181 |
+
<Card.Title>{item.name}</Card.Title>
|
182 |
+
</Col>
|
183 |
+
<Col className="d-flex align-items-center justify-content-center">
|
184 |
+
<Card.Text>Đơn giá: {item.price} VND</Card.Text>
|
185 |
+
</Col>
|
186 |
+
<Col className="d-flex align-items-center justify-content-center">
|
187 |
+
<Card.Text>Số lượng: {item.amount}</Card.Text>
|
188 |
+
</Col>
|
189 |
+
<Col className="d-flex align-items-center justify-content-center">
|
190 |
+
<Card.Text>Tổng cộng: {item.price * item.amount} VND</Card.Text>
|
191 |
+
</Col>
|
192 |
+
</Row>
|
193 |
+
</Card.Body>
|
194 |
+
</Card>
|
195 |
+
</Col>
|
196 |
+
))}
|
197 |
+
<Col xs="12" className="text-center">
|
198 |
+
<Button className="m-3" onClick={handleShow}>
|
199 |
+
Thanh toán
|
200 |
+
</Button>
|
201 |
+
<Button as="a" href="/menu" variant="outline-primary" className="m-3">
|
202 |
+
Xem menu
|
203 |
+
</Button>
|
204 |
+
</Col>
|
205 |
+
</Row>
|
206 |
+
</Container>
|
207 |
+
) : (
|
208 |
+
<div className="text-center">
|
209 |
+
<p className="text-center my-3">Giỏ hàng của bạn hiện đang trống.</p>
|
210 |
+
<Button as='a' href='/menu'>Xem menu</Button>
|
211 |
+
</div>
|
212 |
+
)}
|
213 |
+
</Container>
|
214 |
+
)
|
215 |
+
} />
|
216 |
+
)
|
217 |
+
}
|
frontend/src/pages/{HomePage.js → user-pages/HomePage.js}
RENAMED
@@ -1,10 +1,10 @@
|
|
1 |
// pages/HomePage.js
|
2 |
// import React, { useState } from 'react';
|
3 |
-
import AboutUsSection from '
|
4 |
-
import NewsSection from '
|
5 |
-
import StoreSection from '
|
6 |
-
import MenuSection from '
|
7 |
-
import BasicTemplate from '
|
8 |
|
9 |
function HomePage () {
|
10 |
// const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
|
1 |
// pages/HomePage.js
|
2 |
// import React, { useState } from 'react';
|
3 |
+
import AboutUsSection from '../../molecules/AboutUsSection';
|
4 |
+
import NewsSection from '../../organisms/NewsSection';
|
5 |
+
import StoreSection from '../../organisms/StoreSection';
|
6 |
+
import MenuSection from '../../organisms/MenuSection';
|
7 |
+
import BasicTemplate from '../../templates/BasicTemplate';
|
8 |
|
9 |
function HomePage () {
|
10 |
// const [isLoggedIn, setIsLoggedIn] = useState(false);
|
frontend/src/pages/{LoginPage.js → user-pages/LoginPage.js}
RENAMED
@@ -1,10 +1,10 @@
|
|
1 |
import { useState } from "react";
|
2 |
import { Alert, Container, Form, Row, Col, Button, Card } from "react-bootstrap";
|
3 |
-
import BasicTemplate from "
|
4 |
import { useNavigate } from "react-router-dom";
|
5 |
import axios from 'axios';
|
6 |
-
import jwtDecoder from "
|
7 |
-
import DataStorage from "
|
8 |
|
9 |
export default function LoginPage() {
|
10 |
|
|
|
1 |
import { useState } from "react";
|
2 |
import { Alert, Container, Form, Row, Col, Button, Card } from "react-bootstrap";
|
3 |
+
import BasicTemplate from "../../templates/BasicTemplate";
|
4 |
import { useNavigate } from "react-router-dom";
|
5 |
import axios from 'axios';
|
6 |
+
import jwtDecoder from "../../organisms/jwtDecoder";
|
7 |
+
import DataStorage from "../../organisms/DataStorage";
|
8 |
|
9 |
export default function LoginPage() {
|
10 |
|
frontend/src/pages/{MenuPage.js → user-pages/MenuPage.js}
RENAMED
@@ -1,9 +1,9 @@
|
|
1 |
import { useState, useEffect } from 'react';
|
2 |
import { Modal, Button, Container, Row, Col, Tab, Tabs, Form, InputGroup } from 'react-bootstrap';
|
3 |
-
import MenuItem from '
|
4 |
-
import BasicTemplate from '
|
5 |
-
import DataStorage from '
|
6 |
-
import CacheStorage from '
|
7 |
import axios from 'axios';
|
8 |
|
9 |
const categoryMapper = [
|
@@ -17,6 +17,12 @@ function MenuPage() {
|
|
17 |
const [selectedDish, setSelectedDish] = useState(null);
|
18 |
const [show, setShow] = useState(false);
|
19 |
const [cartAmount, setCartAmount] = useState(0);
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
const [loading, setLoading] = useState(true);
|
21 |
const [menuItems, setMenuItems] = useState({});
|
22 |
|
@@ -24,7 +30,7 @@ function MenuPage() {
|
|
24 |
const cart = JSON.parse(DataStorage.get('cart')) || {}; // Đảm bảo cart không null
|
25 |
|
26 |
cart[selectedDish.id] = {
|
27 |
-
'name':selectedDish.item_name,
|
28 |
'amount': cartAmount,
|
29 |
'imageSrc': selectedDish.image_url,
|
30 |
'price': selectedDish.price
|
@@ -43,7 +49,7 @@ function MenuPage() {
|
|
43 |
}
|
44 |
|
45 |
function handleShow(dish) {
|
46 |
-
if (DataStorage.get('isLoggedIn')
|
47 |
setShow(true);
|
48 |
return;
|
49 |
} else {
|
@@ -53,7 +59,7 @@ function MenuPage() {
|
|
53 |
const cart = JSON.parse(DataStorage.get('cart')) || {};
|
54 |
|
55 |
// Kiểm tra số lượng món ăn trong cart
|
56 |
-
const amount = cart[dish.id] !== undefined ? cart[dish.id] : 0;
|
57 |
setCartAmount(amount); // Cập nhật số lượng
|
58 |
setShow(true); // Hiển thị modal
|
59 |
}
|
@@ -70,38 +76,139 @@ function MenuPage() {
|
|
70 |
}
|
71 |
|
72 |
useEffect(() => {
|
73 |
-
const
|
74 |
try {
|
75 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
76 |
|
77 |
-
|
|
|
|
|
|
|
|
|
|
|
78 |
// Gọi API để lấy món theo itemType
|
79 |
-
const response = await axios.get(process.env.REACT_APP_API_URL + `/
|
80 |
// Lưu danh sách món theo itemType
|
81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
}
|
83 |
-
console.log(menuItemsByType);
|
84 |
-
setMenuItems(menuItemsByType);
|
85 |
-
setLoading(false);
|
86 |
-
CacheStorage.set('menuItems',JSON.stringify(menuItemsByType));
|
87 |
-
} catch (error) {
|
88 |
-
console.error('Error fetching menu items:', error);
|
89 |
-
setLoading(false);
|
90 |
}
|
91 |
-
}
|
92 |
-
|
93 |
-
|
94 |
-
|
|
|
95 |
setLoading(false);
|
96 |
} else {
|
97 |
-
|
98 |
}
|
|
|
99 |
|
100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
101 |
|
102 |
let modalContent;
|
103 |
|
104 |
-
if (DataStorage.get('isLoggedIn')
|
105 |
modalContent = (
|
106 |
<Modal show={show} onHide={notLoggedInClose} className='text-center align-items-center'>
|
107 |
<Modal.Header closeButton className='text-center'>
|
@@ -172,7 +279,7 @@ function MenuPage() {
|
|
172 |
<Tab eventKey={index} title={category.category}>
|
173 |
<Container fluid className='my-5'>
|
174 |
<Row md={3} className="g-4">
|
175 |
-
{menuItems[category.itemType]
|
176 |
<Col key={item.id}>
|
177 |
<div onClick={() => handleShow(item)} className="text-center">
|
178 |
<MenuItem
|
@@ -191,17 +298,24 @@ function MenuPage() {
|
|
191 |
}
|
192 |
|
193 |
return <BasicTemplate content={(
|
194 |
-
<Container fluid className='my-5' style={{ minHeight: '70vh' }}>
|
195 |
-
|
196 |
{modalContent}
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
<Col xs=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
202 |
{menuContent}
|
203 |
</Col>
|
204 |
-
<Col xs={1} md={2}></Col>
|
205 |
</Row>
|
206 |
</Container>
|
207 |
|
|
|
1 |
import { useState, useEffect } from 'react';
|
2 |
import { Modal, Button, Container, Row, Col, Tab, Tabs, Form, InputGroup } from 'react-bootstrap';
|
3 |
+
import MenuItem from '../../molecules/MenuItem';
|
4 |
+
import BasicTemplate from '../../templates/BasicTemplate';
|
5 |
+
import DataStorage from '../../organisms/DataStorage';
|
6 |
+
import CacheStorage from '../../organisms/CacheStorage';
|
7 |
import axios from 'axios';
|
8 |
|
9 |
const categoryMapper = [
|
|
|
17 |
const [selectedDish, setSelectedDish] = useState(null);
|
18 |
const [show, setShow] = useState(false);
|
19 |
const [cartAmount, setCartAmount] = useState(0);
|
20 |
+
|
21 |
+
const [branches, setBranches] = useState([]);
|
22 |
+
const defaultSelectedStore = CacheStorage.get('selectedStore') || "";
|
23 |
+
const [selectedStore, setSelectedStore] = useState(defaultSelectedStore);
|
24 |
+
|
25 |
+
const [loadingBranches, setLoadingBranches] = useState(true);
|
26 |
const [loading, setLoading] = useState(true);
|
27 |
const [menuItems, setMenuItems] = useState({});
|
28 |
|
|
|
30 |
const cart = JSON.parse(DataStorage.get('cart')) || {}; // Đảm bảo cart không null
|
31 |
|
32 |
cart[selectedDish.id] = {
|
33 |
+
'name': selectedDish.item_name,
|
34 |
'amount': cartAmount,
|
35 |
'imageSrc': selectedDish.image_url,
|
36 |
'price': selectedDish.price
|
|
|
49 |
}
|
50 |
|
51 |
function handleShow(dish) {
|
52 |
+
if (DataStorage.get('isLoggedIn') !== 'true' || !DataStorage.get('cart')) {
|
53 |
setShow(true);
|
54 |
return;
|
55 |
} else {
|
|
|
59 |
const cart = JSON.parse(DataStorage.get('cart')) || {};
|
60 |
|
61 |
// Kiểm tra số lượng món ăn trong cart
|
62 |
+
const amount = cart[dish.id] !== undefined ? cart[dish.id].amount : 0;
|
63 |
setCartAmount(amount); // Cập nhật số lượng
|
64 |
setShow(true); // Hiển thị modal
|
65 |
}
|
|
|
76 |
}
|
77 |
|
78 |
useEffect(() => {
|
79 |
+
const fetchBranches = async () => {
|
80 |
try {
|
81 |
+
const response = await axios.get(process.env.REACT_APP_API_URL + '/branchs'); // Thay 'API_ENDPOINT' bằng URL của API
|
82 |
+
setBranches(response.data); // Lưu dữ liệu vào state
|
83 |
+
setLoadingBranches(false); // Đặt loading thành false khi hoàn tất
|
84 |
+
CacheStorage.set('stores', JSON.stringify(Object(response.data)));
|
85 |
+
} catch (error) {
|
86 |
+
console.error('Error fetching branches:', error);
|
87 |
+
setLoadingBranches(false); // Đặt loading thành false nếu lỗi
|
88 |
+
}
|
89 |
+
};
|
90 |
+
if (CacheStorage.get('stores')) {
|
91 |
+
setBranches(JSON.parse(CacheStorage.get('stores')));
|
92 |
+
setLoadingBranches(false);
|
93 |
+
} else {
|
94 |
+
fetchBranches();
|
95 |
+
}
|
96 |
+
}, [])
|
97 |
+
|
98 |
+
// useEffect(() => {
|
99 |
+
// const fetchMenuItems = async () => {
|
100 |
+
// try {
|
101 |
+
// const menuItemsByType = {};
|
102 |
+
|
103 |
+
// for (const item of categoryMapper) {
|
104 |
+
// // Gọi API để lấy món theo itemType
|
105 |
+
// const response = await axios.get(process.env.REACT_APP_API_URL + `/menu-items?filter.item_type=${item.itemType}`);
|
106 |
+
// // Lưu danh sách món theo itemType
|
107 |
+
// menuItemsByType[item.itemType] = response.data.data;
|
108 |
+
// }
|
109 |
+
// console.log(menuItemsByType);
|
110 |
+
// setMenuItems(menuItemsByType);
|
111 |
+
// setLoading(false);
|
112 |
+
// CacheStorage.set('menuItems',JSON.stringify(menuItemsByType));
|
113 |
+
// } catch (error) {
|
114 |
+
// console.error('Error fetching menu items:', error);
|
115 |
+
// setLoading(false);
|
116 |
+
// }
|
117 |
+
// };
|
118 |
+
|
119 |
+
// if (CacheStorage.get('menuItems')) {
|
120 |
+
// setMenuItems(JSON.parse(CacheStorage.get('menuItems')));
|
121 |
+
// setLoading(false);
|
122 |
+
// } else {
|
123 |
+
// fetchMenuItems();
|
124 |
+
// }
|
125 |
+
|
126 |
+
// }, []);
|
127 |
+
|
128 |
+
function organizeMenuItemsByType(menuData) {
|
129 |
+
// Lấy ra tất cả menu_item từ array ban đầu
|
130 |
+
const menuItems = menuData.map(item => item.menu_item);
|
131 |
+
|
132 |
+
// Tạo một object để nhóm menu_item theo item_type, khởi tạo các mảng rỗng cho mỗi item_type từ categoryMapper
|
133 |
+
const organizedItems = categoryMapper.reduce((acc, { itemType }) => {
|
134 |
+
acc[itemType] = [];
|
135 |
+
return acc;
|
136 |
+
}, {});
|
137 |
+
|
138 |
+
// Thêm menu_item vào organizedItems theo item_type
|
139 |
+
menuItems.forEach(menuItem => {
|
140 |
+
const type = menuItem.item_type;
|
141 |
+
|
142 |
+
// Đưa menu_item vào array của item_type tương ứng
|
143 |
+
if (organizedItems[type]) {
|
144 |
+
organizedItems[type].push(menuItem);
|
145 |
+
}
|
146 |
+
});
|
147 |
+
|
148 |
+
return organizedItems;
|
149 |
+
}
|
150 |
|
151 |
+
useEffect(() => {
|
152 |
+
const fetchStoreData = async (storeId) => {
|
153 |
+
if (storeId!=="") {
|
154 |
+
try {
|
155 |
+
let menuItemsByType = {};
|
156 |
+
|
157 |
// Gọi API để lấy món theo itemType
|
158 |
+
const response = await axios.get(process.env.REACT_APP_API_URL + `/branchs/${storeId}/menus?limit=100`);
|
159 |
// Lưu danh sách món theo itemType
|
160 |
+
console.log('Response', response.data.data);
|
161 |
+
menuItemsByType = organizeMenuItemsByType(response.data.data);
|
162 |
+
|
163 |
+
console.log(menuItemsByType);
|
164 |
+
setMenuItems(menuItemsByType);
|
165 |
+
setLoading(false);
|
166 |
+
CacheStorage.set(`store_menu_${storeId}`, JSON.stringify(menuItemsByType));
|
167 |
+
} catch (error) {
|
168 |
+
console.error('Error fetching menu items:', error);
|
169 |
+
setLoading(false);
|
170 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
}
|
172 |
+
}
|
173 |
+
// Kiểm tra cache thông tin chi nhánh trong sessionStorage
|
174 |
+
const cachedStoreData = CacheStorage.get(`store_menu_${selectedStore}`);
|
175 |
+
if (cachedStoreData) {
|
176 |
+
setMenuItems(JSON.parse(cachedStoreData));
|
177 |
setLoading(false);
|
178 |
} else {
|
179 |
+
fetchStoreData(selectedStore);
|
180 |
}
|
181 |
+
}, [selectedStore]);
|
182 |
|
183 |
+
const handleSelectStore = (e) => {
|
184 |
+
setSelectedStore(e.target.value);
|
185 |
+
CacheStorage.set('selectedStore',e.target.value);
|
186 |
+
setLoading(true);
|
187 |
+
};
|
188 |
+
|
189 |
+
let selectboxContent;
|
190 |
+
|
191 |
+
if (loadingBranches) {
|
192 |
+
selectboxContent = (<p>Đang tải dữ liệu...</p>)
|
193 |
+
} else {
|
194 |
+
selectboxContent = (<Form>
|
195 |
+
<Form.Group controlId="branchSelect">
|
196 |
+
<Form.Label>Chọn chi nhánh:</Form.Label>
|
197 |
+
<Form.Control as="select" onChange={handleSelectStore} value={selectedStore}>
|
198 |
+
<option value="">-- Chọn chi nhánh --</option>
|
199 |
+
{branches.map((store) => (
|
200 |
+
<option key={store.id} value={store.id}>
|
201 |
+
{store.name + ' - ' + store.location}
|
202 |
+
</option>
|
203 |
+
))}
|
204 |
+
</Form.Control>
|
205 |
+
</Form.Group>
|
206 |
+
</Form>);
|
207 |
+
}
|
208 |
|
209 |
let modalContent;
|
210 |
|
211 |
+
if (DataStorage.get('isLoggedIn') !== 'true' || !DataStorage.get('cart')) {
|
212 |
modalContent = (
|
213 |
<Modal show={show} onHide={notLoggedInClose} className='text-center align-items-center'>
|
214 |
<Modal.Header closeButton className='text-center'>
|
|
|
279 |
<Tab eventKey={index} title={category.category}>
|
280 |
<Container fluid className='my-5'>
|
281 |
<Row md={3} className="g-4">
|
282 |
+
{menuItems[category.itemType]?.map((item, idx) => (
|
283 |
<Col key={item.id}>
|
284 |
<div onClick={() => handleShow(item)} className="text-center">
|
285 |
<MenuItem
|
|
|
298 |
}
|
299 |
|
300 |
return <BasicTemplate content={(
|
301 |
+
<Container fluid className='d-flex my-5 justify-content-center' style={{ minHeight: '70vh' }}>
|
302 |
+
<div className='align-items-center'>
|
303 |
{modalContent}
|
304 |
+
</div>
|
305 |
+
|
306 |
+
{/* selectbox branches */}
|
307 |
+
<Row style={{ maxWidth: "80vw" }}>
|
308 |
+
<Col xs='12'>
|
309 |
+
{selectboxContent}
|
310 |
+
</Col>
|
311 |
+
<Col xs='12' className='my-5'>
|
312 |
+
<h1 className='text-center'>Thực đơn</h1>
|
313 |
+
</Col>
|
314 |
+
{/* <Col xs={1} md={2}></Col> */}
|
315 |
+
<Col className='text-center'>
|
316 |
{menuContent}
|
317 |
</Col>
|
318 |
+
{/* <Col xs={1} md={2}></Col> */}
|
319 |
</Row>
|
320 |
</Container>
|
321 |
|
frontend/src/pages/{NewsPage.js → user-pages/NewsPage.js}
RENAMED
@@ -1,7 +1,7 @@
|
|
1 |
import {Container, Row, Col} from "react-bootstrap";
|
2 |
-
import BasicTemplate from "
|
3 |
import { useSearchParams } from 'react-router-dom';
|
4 |
-
import CacheStorage from "
|
5 |
|
6 |
export default function NewsPage() {
|
7 |
|
@@ -11,8 +11,6 @@ export default function NewsPage() {
|
|
11 |
|
12 |
const feedDetail = Array.from(JSON.parse(CacheStorage.get('feeds'))).filter((item) => item.id === newsId);
|
13 |
|
14 |
-
|
15 |
-
|
16 |
const newsTitle = feedDetail[0].title;
|
17 |
const newsContent = feedDetail[0].description;
|
18 |
const newsImageSrc = feedDetail[0].image_url;
|
|
|
1 |
import {Container, Row, Col} from "react-bootstrap";
|
2 |
+
import BasicTemplate from "../../templates/BasicTemplate";
|
3 |
import { useSearchParams } from 'react-router-dom';
|
4 |
+
import CacheStorage from "../../organisms/CacheStorage";
|
5 |
|
6 |
export default function NewsPage() {
|
7 |
|
|
|
11 |
|
12 |
const feedDetail = Array.from(JSON.parse(CacheStorage.get('feeds'))).filter((item) => item.id === newsId);
|
13 |
|
|
|
|
|
14 |
const newsTitle = feedDetail[0].title;
|
15 |
const newsContent = feedDetail[0].description;
|
16 |
const newsImageSrc = feedDetail[0].image_url;
|
frontend/src/pages/user-pages/PaymentSuccessPage.js
ADDED
File without changes
|
frontend/src/pages/{RegisterPage.js → user-pages/RegisterPage.js}
RENAMED
@@ -2,10 +2,10 @@ import React, { useState } from 'react';
|
|
2 |
import validator from 'validator';
|
3 |
import { useNavigate } from 'react-router-dom';
|
4 |
import { Container, Form, Button, Alert, Row, Col, Card } from 'react-bootstrap';
|
5 |
-
import BasicTemplate from '
|
6 |
import axios from 'axios';
|
7 |
-
import DataStorage from '
|
8 |
-
import jwtDecoder from '
|
9 |
|
10 |
const RegisterPage = () => {
|
11 |
const [full_name, setFullname] = useState('');
|
|
|
2 |
import validator from 'validator';
|
3 |
import { useNavigate } from 'react-router-dom';
|
4 |
import { Container, Form, Button, Alert, Row, Col, Card } from 'react-bootstrap';
|
5 |
+
import BasicTemplate from '../../templates/BasicTemplate';
|
6 |
import axios from 'axios';
|
7 |
+
import DataStorage from '../../organisms/DataStorage';
|
8 |
+
import jwtDecoder from '../../organisms/jwtDecoder';
|
9 |
|
10 |
const RegisterPage = () => {
|
11 |
const [full_name, setFullname] = useState('');
|
frontend/src/pages/{UserInfoPage.js → user-pages/UserInfoPage.js}
RENAMED
@@ -1,9 +1,9 @@
|
|
1 |
-
import BasicTemplate from "
|
2 |
import { Container, Form, Row, Col, Card, Alert, Button, Image } from "react-bootstrap";
|
3 |
import React, { useState, useEffect } from "react";
|
4 |
import validator from "validator";
|
5 |
import axios from "axios";
|
6 |
-
import DataStorage from "
|
7 |
|
8 |
export default function UserInfoPage() {
|
9 |
|
@@ -66,20 +66,14 @@ export default function UserInfoPage() {
|
|
66 |
};
|
67 |
|
68 |
// Hàm xử lý thay đổi avatar
|
69 |
-
const
|
70 |
-
const
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
}));
|
78 |
-
};
|
79 |
-
reader.readAsDataURL(file);
|
80 |
-
setChanged(true);
|
81 |
-
}
|
82 |
-
};
|
83 |
|
84 |
useEffect(() => {
|
85 |
const hasChanges = () => {
|
@@ -120,6 +114,7 @@ export default function UserInfoPage() {
|
|
120 |
let data = {};
|
121 |
|
122 |
// if (avatarBase64) data.avatar = avatarBase64;
|
|
|
123 |
if (formData.full_name.trim() !== "" && formData.full_name.trim() !== initialData.full_name) data.full_name = formData.full_name.trim();
|
124 |
if (formData.phone_number.trim() !== "" && formData.phone_number.trim() !== initialData.phone_number) data.phone_number = formData.phone_number.trim();
|
125 |
if (formData.address.trim() !== "" && formData.address.trim() !== initialData.address) data.address = formData.address.trim();
|
@@ -145,9 +140,9 @@ export default function UserInfoPage() {
|
|
145 |
(
|
146 |
<Container fluid className="d-flex align-items-center justify-content-center mt-5">
|
147 |
<Row>
|
148 |
-
<Col xs={1} md={2}></Col>
|
149 |
<Col xs={10} md={8}>
|
150 |
-
<Card style={{ width: '
|
151 |
<Card.Header>
|
152 |
<Card.Title className='mt-1 text-center'>Thông tin khách hàng</Card.Title>
|
153 |
</Card.Header>
|
@@ -156,22 +151,31 @@ export default function UserInfoPage() {
|
|
156 |
<Form onSubmit={handleSubmit}>
|
157 |
{error && <Alert variant="danger">{error}</Alert>}
|
158 |
<Row className="mb-3">
|
159 |
-
|
160 |
-
|
161 |
-
<
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
|
|
|
|
|
|
169 |
<Form.Label>Ảnh đại diện</Form.Label>
|
170 |
-
<Form.Control
|
|
|
|
|
|
|
|
|
|
|
171 |
</Form.Group>
|
172 |
</Col>
|
173 |
-
|
174 |
-
|
|
|
175 |
<Form.Label>Họ tên người dùng</Form.Label>
|
176 |
<Form.Control
|
177 |
type="text"
|
@@ -247,10 +251,8 @@ export default function UserInfoPage() {
|
|
247 |
</Card>
|
248 |
|
249 |
</Col>
|
250 |
-
<Col xs={1} md={2}></Col>
|
251 |
</Row>
|
252 |
-
|
253 |
-
|
254 |
</Container>
|
255 |
)
|
256 |
} />
|
|
|
1 |
+
import BasicTemplate from "../../templates/BasicTemplate";
|
2 |
import { Container, Form, Row, Col, Card, Alert, Button, Image } from "react-bootstrap";
|
3 |
import React, { useState, useEffect } from "react";
|
4 |
import validator from "validator";
|
5 |
import axios from "axios";
|
6 |
+
import DataStorage from "../../organisms/DataStorage";
|
7 |
|
8 |
export default function UserInfoPage() {
|
9 |
|
|
|
66 |
};
|
67 |
|
68 |
// Hàm xử lý thay đổi avatar
|
69 |
+
const handleAvatarUrlChange = (e) => {
|
70 |
+
const url = e.target.value;
|
71 |
+
setFormData((prevData) => ({
|
72 |
+
...prevData,
|
73 |
+
avatar: url // Cập nhật URL ảnh
|
74 |
+
}));
|
75 |
+
setChanged(true);
|
76 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
77 |
|
78 |
useEffect(() => {
|
79 |
const hasChanges = () => {
|
|
|
114 |
let data = {};
|
115 |
|
116 |
// if (avatarBase64) data.avatar = avatarBase64;
|
117 |
+
if (formData.avatar.trim() !== "") data.avatar = formData.avatar.trim();
|
118 |
if (formData.full_name.trim() !== "" && formData.full_name.trim() !== initialData.full_name) data.full_name = formData.full_name.trim();
|
119 |
if (formData.phone_number.trim() !== "" && formData.phone_number.trim() !== initialData.phone_number) data.phone_number = formData.phone_number.trim();
|
120 |
if (formData.address.trim() !== "" && formData.address.trim() !== initialData.address) data.address = formData.address.trim();
|
|
|
140 |
(
|
141 |
<Container fluid className="d-flex align-items-center justify-content-center mt-5">
|
142 |
<Row>
|
143 |
+
{/* <Col xs={1} md={2}></Col> */}
|
144 |
<Col xs={10} md={8}>
|
145 |
+
<Card style={{ width: '40vw' }} className='justify-content-center'>
|
146 |
<Card.Header>
|
147 |
<Card.Title className='mt-1 text-center'>Thông tin khách hàng</Card.Title>
|
148 |
</Card.Header>
|
|
|
151 |
<Form onSubmit={handleSubmit}>
|
152 |
{error && <Alert variant="danger">{error}</Alert>}
|
153 |
<Row className="mb-3">
|
154 |
+
|
155 |
+
<Col xs={12} md={6} className="d-flex flex-column align-items-center">
|
156 |
+
<div className="avatar-container mb-4">
|
157 |
+
<Image
|
158 |
+
src={formData.avatar}
|
159 |
+
alt="User Avatar"
|
160 |
+
roundedCircle
|
161 |
+
width="100%"
|
162 |
+
height="100%"
|
163 |
+
style={{ objectFit: 'cover' }}
|
164 |
+
/>
|
165 |
+
</div>
|
166 |
+
<Form.Group controlId="formAvatar" className="mx-3" style={{ width: '100%' }}>
|
167 |
<Form.Label>Ảnh đại diện</Form.Label>
|
168 |
+
<Form.Control
|
169 |
+
type="text"
|
170 |
+
placeholder="Nhập URL ảnh"
|
171 |
+
value={formData.avatar || ''}
|
172 |
+
onChange={handleAvatarUrlChange} // Gọi hàm khi URL thay đổi
|
173 |
+
/>
|
174 |
</Form.Group>
|
175 |
</Col>
|
176 |
+
|
177 |
+
<Col xs={12} md={6} className="d-flex flex-column mt-5 align-items-center">
|
178 |
+
<Form.Group controlId="formUsername" style={{ width: '80%' }}>
|
179 |
<Form.Label>Họ tên người dùng</Form.Label>
|
180 |
<Form.Control
|
181 |
type="text"
|
|
|
251 |
</Card>
|
252 |
|
253 |
</Col>
|
254 |
+
{/* <Col xs={1} md={2}></Col> */}
|
255 |
</Row>
|
|
|
|
|
256 |
</Container>
|
257 |
)
|
258 |
} />
|
frontend/src/styles/styles.css
CHANGED
@@ -114,6 +114,10 @@ body {
|
|
114 |
border-color: #e99e40;
|
115 |
}
|
116 |
|
|
|
|
|
|
|
|
|
117 |
/* .btn-outline-primary {
|
118 |
color: var(--primary-color);
|
119 |
border-color: var(--primary-color);
|
@@ -191,6 +195,11 @@ body {
|
|
191 |
transform: scale(1.00);
|
192 |
}
|
193 |
|
|
|
|
|
|
|
|
|
|
|
194 |
/* Container cho các phần */
|
195 |
.menu-section {
|
196 |
padding: 40px 0;
|
@@ -244,4 +253,69 @@ a:active {
|
|
244 |
|
245 |
.modal-content {
|
246 |
background-color: var(--modal-background-color);
|
247 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
114 |
border-color: #e99e40;
|
115 |
}
|
116 |
|
117 |
+
.btn-primary:disabled {
|
118 |
+
background-color: #755b37;
|
119 |
+
border-color: #755b37
|
120 |
+
}
|
121 |
/* .btn-outline-primary {
|
122 |
color: var(--primary-color);
|
123 |
border-color: var(--primary-color);
|
|
|
195 |
transform: scale(1.00);
|
196 |
}
|
197 |
|
198 |
+
.card-report {
|
199 |
+
background: linear-gradient(rgba(150, 150, 150, 0.8), rgba(150, 150, 150, 0.95));
|
200 |
+
box-shadow: none;
|
201 |
+
}
|
202 |
+
|
203 |
/* Container cho các phần */
|
204 |
.menu-section {
|
205 |
padding: 40px 0;
|
|
|
253 |
|
254 |
.modal-content {
|
255 |
background-color: var(--modal-background-color);
|
256 |
+
}
|
257 |
+
|
258 |
+
/* Tùy chỉnh nền và viền của selectbox */
|
259 |
+
#branchSelect {
|
260 |
+
background-color: #3333338d;
|
261 |
+
/* Màu nền tối để phù hợp với giao diện tổng thể */
|
262 |
+
color: #fff;
|
263 |
+
/* Màu chữ sáng để tương phản */
|
264 |
+
border: 1px solid #666;
|
265 |
+
/* Màu viền trung tính */
|
266 |
+
border-radius: 8px;
|
267 |
+
/* Bo góc mềm mại */
|
268 |
+
padding: 10px;
|
269 |
+
/* Khoảng cách bên trong */
|
270 |
+
width: 100%;
|
271 |
+
/* max-width: 400px; */
|
272 |
+
/* Giới hạn chiều rộng tối đa */
|
273 |
+
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
|
274 |
+
/* Đổ bóng nhẹ */
|
275 |
+
-webkit-appearance: none;
|
276 |
+
/* Ẩn mũi tên mặc định */
|
277 |
+
-moz-appearance: none;
|
278 |
+
appearance: none;
|
279 |
+
cursor: pointer;
|
280 |
+
}
|
281 |
+
|
282 |
+
/* Tùy chỉnh mũi tên của selectbox */
|
283 |
+
#branchSelect::after {
|
284 |
+
content: '▼';
|
285 |
+
/* Thêm ký tự mũi tên */
|
286 |
+
color: #ccc;
|
287 |
+
/* Màu mũi tên */
|
288 |
+
padding-left: 10px;
|
289 |
+
position: absolute;
|
290 |
+
right: 15px;
|
291 |
+
top: 50%;
|
292 |
+
transform: translateY(-50%);
|
293 |
+
pointer-events: none;
|
294 |
+
}
|
295 |
+
|
296 |
+
/* Điều chỉnh khi selectbox được focus */
|
297 |
+
#branchSelect:focus {
|
298 |
+
outline: none;
|
299 |
+
border-color: #ffa500;
|
300 |
+
/* Thay đổi màu viền khi focus */
|
301 |
+
box-shadow: 0px 2px 8px rgba(255, 165, 0, 0.5);
|
302 |
+
/* Hiệu ứng đổ bóng nổi bật */
|
303 |
+
}
|
304 |
+
|
305 |
+
/* Tùy chỉnh các tùy chọn (option) */
|
306 |
+
#branchSelect option {
|
307 |
+
background-color: #333;
|
308 |
+
color: #fff;
|
309 |
+
}
|
310 |
+
|
311 |
+
.avatar-container {
|
312 |
+
width: 200px; /* Đặt kích thước theo ý muốn */
|
313 |
+
height: 200px;
|
314 |
+
border-radius: 50%; /* Biến khung thành hình tròn */
|
315 |
+
overflow: hidden; /* Ẩn phần ảnh vượt ra ngoài khung */
|
316 |
+
display: flex;
|
317 |
+
align-items: center;
|
318 |
+
justify-content: center;
|
319 |
+
background-color: #f0f0f0; /* Màu nền để hiển thị nếu ảnh chưa tải */
|
320 |
+
background-image: url('../../public/default_avatar.jpg');
|
321 |
+
}
|