tosanoob commited on
Commit
0d37b12
1 Parent(s): fdbdf19

update a lot

Browse files
Files changed (36) hide show
  1. frontend/.gitignore +3 -1
  2. frontend/package-lock.json +662 -0
  3. frontend/package.json +2 -0
  4. frontend/src/index.js +40 -15
  5. frontend/src/molecules/AdminNavBar.js +1 -0
  6. frontend/src/molecules/MenuItem.js +22 -10
  7. frontend/src/molecules/StoreItem.js +39 -13
  8. frontend/src/organisms/AdminReport.js +35 -0
  9. frontend/src/organisms/NewsSection.js +8 -1
  10. frontend/src/organisms/PieChartComponent.js +50 -0
  11. frontend/src/organisms/StackedBarChartComponent.js +59 -0
  12. frontend/src/organisms/SummaryReport.js +48 -0
  13. frontend/src/pages/AdminFeedPage.js +0 -22
  14. frontend/src/pages/AdminMenuPage.js +0 -21
  15. frontend/src/pages/CartPage.js +0 -78
  16. frontend/src/pages/admin-pages/AdminBranchesEditPage.js +181 -0
  17. frontend/src/pages/admin-pages/AdminBranchesPage.js +129 -0
  18. frontend/src/pages/admin-pages/AdminFeedPage.js +167 -0
  19. frontend/src/pages/{AdminLoginPage.js → admin-pages/AdminLoginPage.js} +3 -3
  20. frontend/src/pages/admin-pages/AdminMenuEditPage.js +252 -0
  21. frontend/src/pages/admin-pages/AdminMenuPage.js +130 -0
  22. frontend/src/pages/admin-pages/AdminNewsEditPage.js +177 -0
  23. frontend/src/pages/{AdminOrderPage.js → admin-pages/AdminOrderPage.js} +1 -1
  24. frontend/src/pages/{AdminSchedulePage.js → admin-pages/AdminSchedulePage.js} +1 -1
  25. frontend/src/pages/{AdminStaffPage.js → admin-pages/AdminStaffPage.js} +1 -1
  26. frontend/src/pages/{AdminSummaryPage.js → admin-pages/AdminSummaryPage.js} +8 -3
  27. frontend/src/pages/{AdminUserInfoPage.js → admin-pages/AdminUserInfoPage.js} +2 -2
  28. frontend/src/pages/user-pages/CartPage.js +217 -0
  29. frontend/src/pages/{HomePage.js → user-pages/HomePage.js} +5 -5
  30. frontend/src/pages/{LoginPage.js → user-pages/LoginPage.js} +3 -3
  31. frontend/src/pages/{MenuPage.js → user-pages/MenuPage.js} +149 -35
  32. frontend/src/pages/{NewsPage.js → user-pages/NewsPage.js} +2 -4
  33. frontend/src/pages/user-pages/PaymentSuccessPage.js +0 -0
  34. frontend/src/pages/{RegisterPage.js → user-pages/RegisterPage.js} +3 -3
  35. frontend/src/pages/{UserInfoPage.js → user-pages/UserInfoPage.js} +36 -34
  36. 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( {dishName, description, imageSrc} ) {
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
- </Card.Body>
 
 
 
 
 
 
 
 
 
 
 
 
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( {storeName, address, imageSrc} ) {
5
- return (
6
- <Card className="align-items-center">
7
- <Card.Img variant="top" src={imageSrc}/>
8
- <Card.Body>
9
- <Card.Title>{storeName}</Card.Title>
10
- <Card.Text>
11
- {address}
12
- </Card.Text>
13
- </Card.Body>
14
- </Card>
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 "../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
 
 
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 "../templates/AdminTemplate";
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 "../templates/AdminTemplate";
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 "../templates/AdminTemplate";
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 "../templates/AdminTemplate";
3
  import { useNavigate } from "react-router-dom";
4
  import { useEffect } from "react";
5
- import DataStorage from "../organisms/DataStorage";
 
 
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 "../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
 
 
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 '../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);
 
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 "../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
 
 
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 '../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,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') === null) {
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 fetchMenuItems = async () => {
74
  try {
75
- const menuItemsByType = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
- for (const item of categoryMapper) {
 
 
 
 
 
78
  // Gọi API để lấy món theo itemType
79
- const response = await axios.get(process.env.REACT_APP_API_URL + `/menu-items?filter.item_type=${item.itemType}`);
80
  // Lưu danh sách món theo itemType
81
- menuItemsByType[item.itemType] = response.data.data;
 
 
 
 
 
 
 
 
 
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
- if (CacheStorage.get('menuItems')) {
94
- setMenuItems(JSON.parse(CacheStorage.get('menuItems')));
 
95
  setLoading(false);
96
  } else {
97
- fetchMenuItems();
98
  }
 
99
 
100
- }, []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  let modalContent;
103
 
104
- if (DataStorage.get('isLoggedIn') === null) {
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].map((item, idx) => (
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
- <h1 className='text-center mb-5'>Thực đơn</h1>
199
- <Row>
200
- <Col xs={1} md={2}></Col>
201
- <Col xs={10} md={8}>
 
 
 
 
 
 
 
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 "../templates/BasicTemplate";
3
  import { useSearchParams } from 'react-router-dom';
4
- import CacheStorage from "../organisms/CacheStorage";
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 '../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('');
 
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 "../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,20 +66,14 @@ export default function UserInfoPage() {
66
  };
67
 
68
  // Hàm xử lý thay đổi avatar
69
- const handleAvatarChange = (e) => {
70
- const file = e.target.files[0];
71
- if (file) {
72
- const reader = new FileReader();
73
- reader.onloadend = () => {
74
- setFormData((prevData) => ({
75
- ...prevData,
76
- avatar: reader.result // Cập nhật ảnh đại diện
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: '30rem' }} className='justify-content-center'>
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
- <Col xs={12} md={6} className="text-center">
160
- {/*Image*/}
161
- <Image
162
- src={formData.avatar} // Hiển thị ảnh đại diện từ formData
163
- alt="User Avatar"
164
- roundedCircle
165
- width={120}
166
- height={120}
167
- />
168
- <Form.Group controlId="formAvatar">
 
 
 
169
  <Form.Label>Ảnh đại diện</Form.Label>
170
- <Form.Control type="file" accept="image/*" onChange={handleAvatarChange} />
 
 
 
 
 
171
  </Form.Group>
172
  </Col>
173
- <Col xs={12} md={6}>
174
- <Form.Group controlId="formUsername">
 
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
+ }