AtamCow commited on
Commit
7bcbafd
·
2 Parent(s): c04bd33 046673f

Merge remote-tracking branch 'origin/main'

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +2 -0
  2. backend/.env.example +6 -1
  3. backend/package-lock.json +0 -0
  4. backend/package.json +95 -91
  5. backend/src/app.module.ts +45 -43
  6. backend/src/common/enums/MenuItemType.enum.ts +4 -4
  7. backend/src/common/enums/OrderStatus.enum.ts +5 -5
  8. backend/src/common/enums/OrderType.enum.ts +3 -3
  9. backend/src/common/enums/PaymentMethod.enum.ts +3 -3
  10. backend/src/common/enums/VnpCardType.enum.ts +5 -0
  11. backend/src/common/enums/role.enum.ts +7 -7
  12. backend/src/common/interfaces/jwt-payload.interface.ts +5 -0
  13. backend/src/config/config.ts +5 -0
  14. backend/src/entities/branch-menu.entity.ts +3 -0
  15. backend/src/entities/branch.entity.ts +14 -3
  16. backend/src/entities/feed.entity.ts +11 -1
  17. backend/src/entities/menu-item.entity.ts +6 -2
  18. backend/src/entities/order.entity.ts +21 -7
  19. backend/src/entities/payment.entity.ts +27 -7
  20. backend/src/entities/receipt.entity.ts +57 -0
  21. backend/src/entities/user.entity.ts +8 -4
  22. backend/src/migrations/1730547520878-AddReceipt.ts +30 -0
  23. backend/src/migrations/1730549959767-RemoveEnums.ts +36 -0
  24. backend/src/migrations/1730651201156-modify_payment.ts +32 -0
  25. backend/src/migrations/1730865796585-AddBranchAndOrderFields.ts +22 -0
  26. backend/src/migrations/1730895089788-UpdateOrderFieldTime.ts +25 -0
  27. backend/src/modules/authentication/authentication.service.ts +1 -1
  28. backend/src/modules/authentication/authorization/roles.guard.ts +1 -1
  29. backend/src/modules/branch-menus/branch-menus.service.ts +15 -4
  30. backend/src/modules/branch/branch.controller.ts +6 -1
  31. backend/src/modules/branch/branch.service.ts +9 -3
  32. backend/src/modules/branch/dto/create-branch.dto.ts +3 -0
  33. backend/src/modules/branch/dto/update-branch.dto.ts +4 -0
  34. backend/src/modules/menu-item/dto/create-menu-item.dto.ts +2 -2
  35. backend/src/modules/menu-item/dto/update-menu-item.dto.ts +2 -2
  36. backend/src/modules/order/dto/create-order.dto.ts +6 -2
  37. backend/src/modules/order/dto/update-order.dto.ts +14 -0
  38. backend/src/modules/order/order.controller.ts +41 -15
  39. backend/src/modules/order/order.module.ts +3 -2
  40. backend/src/modules/order/order.service.ts +204 -72
  41. backend/src/modules/user/dto/update-users.dto.ts +37 -0
  42. backend/src/modules/user/user.controller.ts +61 -23
  43. backend/src/modules/user/user.service.ts +167 -66
  44. backend/src/payment/dto/create-payment-url.dto.ts +22 -0
  45. backend/src/payment/dto/create-payment.dto..ts +38 -0
  46. backend/src/payment/payment.controller.ts +39 -0
  47. backend/src/payment/payment.module.ts +11 -0
  48. backend/src/payment/payment.service.ts +200 -0
  49. frontend/.gitignore +4 -1
  50. frontend/package-lock.json +662 -0
Dockerfile CHANGED
@@ -7,6 +7,8 @@ WORKDIR /app
7
  # Copy package.json and package-lock.json
8
  COPY backend/package*.json ./
9
 
 
 
10
  # Install dependencies
11
  RUN npm install
12
 
 
7
  # Copy package.json and package-lock.json
8
  COPY backend/package*.json ./
9
 
10
+ RUN npm install -g npm@10.9.0
11
+
12
  # Install dependencies
13
  RUN npm install
14
 
backend/.env.example CHANGED
@@ -4,4 +4,9 @@ DB_USER='pbl6_jw8s_user'
4
  DB_PASSWORD=''
5
  DB_NAME='pbl6_jw8s'
6
  JWT_KEY= ''
7
- DB_SSL_ENABLED=true # default is true to connect with remote database
 
 
 
 
 
 
4
  DB_PASSWORD=''
5
  DB_NAME='pbl6_jw8s'
6
  JWT_KEY= ''
7
+ DB_SSL_ENABLED=true # default is true to connect with remote database
8
+ #Payment
9
+ VNP_TMNCODE = ''
10
+ VNP_HASHSECRET = ''
11
+ VNP_URL = ''
12
+ VNP_RETURNURL = ''
backend/package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
backend/package.json CHANGED
@@ -1,91 +1,95 @@
1
- {
2
- "type": "module",
3
- "name": "backend",
4
- "version": "0.0.1",
5
- "description": "",
6
- "author": "",
7
- "private": true,
8
- "license": "UNLICENSED",
9
- "scripts": {
10
- "build": "nest build",
11
- "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12
- "start": "nest start",
13
- "start:dev": "nest start --watch",
14
- "start:debug": "nest start --debug --watch",
15
- "start:prod": "node dist/main",
16
- "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
17
- "test": "jest",
18
- "test:watch": "jest --watch",
19
- "test:cov": "jest --coverage",
20
- "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
21
- "test:e2e": "jest --config ./test/jest-e2e.json",
22
- "typeorm": "typeorm-ts-node-esm",
23
- "db:migrate:generate": "npm run typeorm migration:generate -- -d ./src/config/typeorm.ts",
24
- "db:migrate:up": "npm run typeorm migration:run -- -d ./src/config/typeorm.ts",
25
- "db:migrate:down": "npm run typeorm migration:revert -- -d ./src/config/typeorm.ts"
26
- },
27
- "dependencies": {
28
- "@nestjs/common": "^10.0.0",
29
- "@nestjs/config": "^3.2.3",
30
- "@nestjs/core": "^10.0.0",
31
- "@nestjs/jwt": "^10.2.0",
32
- "@nestjs/mapped-types": "*",
33
- "@nestjs/passport": "^10.0.3",
34
- "@nestjs/platform-express": "^10.0.0",
35
- "@nestjs/typeorm": "^10.0.2",
36
- "bcrypt": "^5.1.1",
37
- "class-transformer": "^0.5.1",
38
- "class-validator": "^0.14.1",
39
- "dotenv": "^16.4.5",
40
- "mysql2": "^3.11.3",
41
- "nest-access-control": "^3.1.0",
42
- "nestjs-paginate": "^9.3.0",
43
- "passport": "^0.7.0",
44
- "passport-jwt": "^4.0.1",
45
- "passport-local": "^1.0.0",
46
- "pg": "^8.13.0",
47
- "reflect-metadata": "^0.2.0",
48
- "rxjs": "^7.8.1",
49
- "typeorm": "^0.3.20"
50
- },
51
- "devDependencies": {
52
- "@nestjs/cli": "^10.0.0",
53
- "@nestjs/schematics": "^10.0.0",
54
- "@nestjs/testing": "^10.0.0",
55
- "@types/express": "^4.17.17",
56
- "@types/jest": "^29.5.2",
57
- "@types/node": "^20.3.1",
58
- "@types/supertest": "^6.0.0",
59
- "@typescript-eslint/eslint-plugin": "^8.0.0",
60
- "@typescript-eslint/parser": "^8.0.0",
61
- "eslint": "^8.42.0",
62
- "eslint-config-prettier": "^9.0.0",
63
- "eslint-plugin-prettier": "^5.0.0",
64
- "jest": "^29.5.0",
65
- "prettier": "^3.0.0",
66
- "source-map-support": "^0.5.21",
67
- "supertest": "^7.0.0",
68
- "ts-jest": "^29.1.0",
69
- "ts-loader": "^9.4.3",
70
- "ts-node": "^10.9.2",
71
- "tsconfig-paths": "^4.2.0",
72
- "typescript": "^5.1.3"
73
- },
74
- "jest": {
75
- "moduleFileExtensions": [
76
- "js",
77
- "json",
78
- "ts"
79
- ],
80
- "rootDir": "src",
81
- "testRegex": ".*\\.spec\\.ts$",
82
- "transform": {
83
- "^.+\\.(t|j)s$": "ts-jest"
84
- },
85
- "collectCoverageFrom": [
86
- "**/*.(t|j)s"
87
- ],
88
- "coverageDirectory": "../coverage",
89
- "testEnvironment": "node"
90
- }
91
- }
 
 
 
 
 
1
+ {
2
+ "type": "module",
3
+ "name": "backend",
4
+ "version": "0.0.1",
5
+ "description": "",
6
+ "author": "",
7
+ "private": true,
8
+ "license": "UNLICENSED",
9
+ "scripts": {
10
+ "build": "nest build",
11
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12
+ "start": "nest start",
13
+ "start:dev": "nest start --watch",
14
+ "start:debug": "nest start --debug --watch",
15
+ "start:prod": "node dist/main",
16
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
17
+ "test": "jest",
18
+ "test:watch": "jest --watch",
19
+ "test:cov": "jest --coverage",
20
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
21
+ "test:e2e": "jest --config ./test/jest-e2e.json",
22
+ "typeorm": "typeorm-ts-node-esm",
23
+ "db:migrate:generate": "npm run typeorm migration:generate -- -d ./src/config/typeorm.ts",
24
+ "db:migrate:up": "npm run typeorm migration:run -- -d ./src/config/typeorm.ts",
25
+ "db:migrate:down": "npm run typeorm migration:revert -- -d ./src/config/typeorm.ts"
26
+ },
27
+ "dependencies": {
28
+ "@nestjs/common": "^10.0.0",
29
+ "@nestjs/config": "^3.2.3",
30
+ "@nestjs/core": "^10.0.0",
31
+ "@nestjs/jwt": "^10.2.0",
32
+ "@nestjs/mapped-types": "*",
33
+ "@nestjs/passport": "^10.0.3",
34
+ "@nestjs/platform-express": "^10.0.0",
35
+ "@nestjs/typeorm": "^10.0.2",
36
+ "bcrypt": "^5.1.1",
37
+ "class-transformer": "^0.5.1",
38
+ "class-validator": "^0.14.1",
39
+ "dateformat": "^5.0.3",
40
+ "dotenv": "^16.4.5",
41
+ "mysql2": "^3.11.3",
42
+ "nest-access-control": "^3.1.0",
43
+ "nestjs-paginate": "^9.3.0",
44
+ "passport": "^0.7.0",
45
+ "passport-jwt": "^4.0.1",
46
+ "passport-local": "^1.0.0",
47
+ "pg": "^8.13.0",
48
+ "reflect-metadata": "^0.2.0",
49
+ "rxjs": "^7.8.1",
50
+ "typeorm": "^0.3.20",
51
+ "vnpay": "^1.6.0"
52
+ },
53
+ "devDependencies": {
54
+ "@nestjs/cli": "^10.0.0",
55
+ "@nestjs/schematics": "^10.0.0",
56
+ "@nestjs/testing": "^10.0.0",
57
+ "@types/dateformat": "^5.0.2",
58
+ "@types/express": "^4.17.17",
59
+ "@types/jest": "^29.5.2",
60
+ "@types/multer": "^1.4.12",
61
+ "@types/node": "^20.3.1",
62
+ "@types/supertest": "^6.0.0",
63
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
64
+ "@typescript-eslint/parser": "^8.0.0",
65
+ "eslint": "^8.42.0",
66
+ "eslint-config-prettier": "^9.0.0",
67
+ "eslint-plugin-prettier": "^5.0.0",
68
+ "jest": "^29.5.0",
69
+ "prettier": "^3.0.0",
70
+ "source-map-support": "^0.5.21",
71
+ "supertest": "^7.0.0",
72
+ "ts-jest": "^29.1.0",
73
+ "ts-loader": "^9.4.3",
74
+ "ts-node": "^10.9.2",
75
+ "tsconfig-paths": "^4.2.0",
76
+ "typescript": "^5.1.3"
77
+ },
78
+ "jest": {
79
+ "moduleFileExtensions": [
80
+ "js",
81
+ "json",
82
+ "ts"
83
+ ],
84
+ "rootDir": "src",
85
+ "testRegex": ".*\\.spec\\.ts$",
86
+ "transform": {
87
+ "^.+\\.(t|j)s$": "ts-jest"
88
+ },
89
+ "collectCoverageFrom": [
90
+ "**/*.(t|j)s"
91
+ ],
92
+ "coverageDirectory": "../coverage",
93
+ "testEnvironment": "node"
94
+ }
95
+ }
backend/src/app.module.ts CHANGED
@@ -1,43 +1,45 @@
1
- import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
2
- import { AppController } from './app.controller.js';
3
- import { AppService } from './app.service.js';
4
- import { ConfigModule } from '@nestjs/config';
5
- import { TypeOrmModule } from '@nestjs/typeorm';
6
- import { configuration } from './config/config.js';
7
- import { DatabaseConfigService } from './config/database.js';
8
- import { AppLoggerMiddleware } from './common/middlewares/app-logger.middleware.js';
9
- import { DeviceInfoMiddleware } from './common/middlewares/device-info.middleware.js';
10
- import { UserModule } from './modules/user/user.module.js';
11
- import { BranchModule } from './modules/branch/branch.module.js';
12
- import { AuthenticationModule } from './modules/authentication/authentication.module.js';
13
- import { MenuItemModule } from './modules/menu-item/menu-item.module.js';
14
- import { FeedsModule } from './modules/feeds/feeds.module.js';
15
- import { OrderModule } from './modules/order/order.module.js';
16
- import { BranchMenusModule } from './modules/branch-menus/branch-menus.module.js';
17
- @Module({
18
- imports: [
19
- ConfigModule.forRoot({
20
- isGlobal: true,
21
- load: [configuration],
22
- }),
23
- TypeOrmModule.forRootAsync({
24
- imports: [ConfigModule],
25
- useClass: DatabaseConfigService,
26
- }),
27
- UserModule,
28
- BranchModule,
29
- AuthenticationModule,
30
- MenuItemModule,
31
- FeedsModule,
32
- OrderModule,
33
- BranchMenusModule,
34
- ],
35
- controllers: [AppController],
36
- providers: [AppService],
37
- })
38
- export class AppModule implements NestModule {
39
- configure(consumer: MiddlewareConsumer): void {
40
- consumer.apply(AppLoggerMiddleware).forRoutes('*');
41
- consumer.apply(DeviceInfoMiddleware).forRoutes('*');
42
- }
43
- }
 
 
 
1
+ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
2
+ import { AppController } from './app.controller.js';
3
+ import { AppService } from './app.service.js';
4
+ import { ConfigModule } from '@nestjs/config';
5
+ import { TypeOrmModule } from '@nestjs/typeorm';
6
+ import { configuration } from './config/config.js';
7
+ import { DatabaseConfigService } from './config/database.js';
8
+ import { AppLoggerMiddleware } from './common/middlewares/app-logger.middleware.js';
9
+ import { DeviceInfoMiddleware } from './common/middlewares/device-info.middleware.js';
10
+ import { UserModule } from './modules/user/user.module.js';
11
+ import { BranchModule } from './modules/branch/branch.module.js';
12
+ import { AuthenticationModule } from './modules/authentication/authentication.module.js';
13
+ import { MenuItemModule } from './modules/menu-item/menu-item.module.js';
14
+ import { FeedsModule } from './modules/feeds/feeds.module.js';
15
+ import { OrderModule } from './modules/order/order.module.js';
16
+ import { BranchMenusModule } from './modules/branch-menus/branch-menus.module.js';
17
+ import { PaymentModule } from './payment/payment.module.js';
18
+ @Module({
19
+ imports: [
20
+ ConfigModule.forRoot({
21
+ isGlobal: true,
22
+ load: [configuration],
23
+ }),
24
+ TypeOrmModule.forRootAsync({
25
+ imports: [ConfigModule],
26
+ useClass: DatabaseConfigService,
27
+ }),
28
+ UserModule,
29
+ BranchModule,
30
+ AuthenticationModule,
31
+ MenuItemModule,
32
+ FeedsModule,
33
+ OrderModule,
34
+ BranchMenusModule,
35
+ PaymentModule,
36
+ ],
37
+ controllers: [AppController],
38
+ providers: [AppService],
39
+ })
40
+ export class AppModule implements NestModule {
41
+ configure(consumer: MiddlewareConsumer): void {
42
+ consumer.apply(AppLoggerMiddleware).forRoutes('*');
43
+ consumer.apply(DeviceInfoMiddleware).forRoutes('*');
44
+ }
45
+ }
backend/src/common/enums/MenuItemType.enum.ts CHANGED
@@ -1,6 +1,6 @@
1
  export enum MenuItemType {
2
- MON_CHINH = 'monchinh', // món chính
3
- TRANG_MIENG = 'trangmieng', // tráng miệng
4
- GIAI_KHAT = 'giaikhat', // giải khát
5
- KHAC = 'khac', // khác
6
  }
 
1
  export enum MenuItemType {
2
+ KHAC = 0, // khác
3
+ MON_CHINH = 1, // món chính
4
+ GIAI_KHAT = 2, // giải khát
5
+ TRANG_MIENG = 3, // tráng miệng
6
  }
backend/src/common/enums/OrderStatus.enum.ts CHANGED
@@ -1,7 +1,7 @@
1
  export enum OrderStatus {
2
- PENDING = 'pending', // KH đặt hàng đã thanh toán online, nhân viên chưa xác nhận <online>
3
- CONFIRMED = 'confirmed', // nhân viên xác nhậnchuyển sang trạng thái <online>
4
- PREPARING = 'preparing', // nhân viên xác nhận và sang trạng thái preparing này ngay lập tức <online/offline>
5
- DELIVERING = 'delivering', // dang giao hàng <online>
6
- DONE = 'done', // <online, offline>
7
  }
 
1
  export enum OrderStatus {
2
+ PENDING = 0, // Khách hàng đặt hàng chưa thanh toán <online>
3
+ ONLINE_PAID = 1, // Khách hàng đã thanh toán được nhân viên xác nhận <online>
4
+ PREPARING = 2, // nhân viên xác nhận và sang trạng thái preparing <online/offline>
5
+ DELIVERING = 3, // dang giao hàng <online>
6
+ DONE = 4, // <online, offline>
7
  }
backend/src/common/enums/OrderType.enum.ts CHANGED
@@ -1,5 +1,5 @@
1
  export enum OrderType {
2
- TAKE_AWAY = 'take_away',
3
- OFFLINE = 'offline',
4
- ONLINE = 'online',
5
  }
 
1
  export enum OrderType {
2
+ TAKE_AWAY = 0,
3
+ OFFLINE = 1,
4
+ ONLINE = 2,
5
  }
backend/src/common/enums/PaymentMethod.enum.ts CHANGED
@@ -1,5 +1,5 @@
1
  export enum PaymentMethod {
2
- CASH = 'cash',
3
- CARD = 'card',
4
- ONLINE_PAYMENT = 'online_payment',
5
  }
 
1
  export enum PaymentMethod {
2
+ CASH = 0,
3
+ CARD = 1,
4
+ ONLINE_PAYMENT = 2,
5
  }
backend/src/common/enums/VnpCardType.enum.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export enum VnpCardType {
2
+ None = 0,
3
+ ATM = 1,
4
+ QRCODE = 2
5
+ }
backend/src/common/enums/role.enum.ts CHANGED
@@ -1,8 +1,8 @@
1
  export enum Role {
2
- CUSTOMER = 'CUSTOMER',
3
- ADMIN = 'ADMIN',
4
- BRANCH_MANAGER = 'BRANCH_MANAGER',
5
- AREA_MANAGER = 'AREA_MANAGER',
6
- STAFF = 'STAFF',
7
- SHIPPER = 'SHIPPER'
8
- }
 
1
  export enum Role {
2
+ CUSTOMER = 'CUSTOMER',
3
+ ADMIN = 'ADMIN',
4
+ BRANCH_MANAGER = 'BRANCH_MANAGER',
5
+ AREA_MANAGER = 'AREA_MANAGER',
6
+ STAFF = 'STAFF',
7
+ SHIPPER = 'SHIPPER',
8
+ }
backend/src/common/interfaces/jwt-payload.interface.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ interface JwtPayload {
2
+ sub: string; // id người dùng
3
+ role: string; // quyền người dùng
4
+ username: string;
5
+ }
backend/src/config/config.ts CHANGED
@@ -8,5 +8,10 @@ export const configuration = () => {
8
  'db.name': process.env.DB_NAME,
9
  'db.slow_limit': Number(process.env.DB_SLOW_LIMIT) || 500, // ms
10
  'db.ssl_enabled': process.env.DB_SSL_ENABLED,
 
 
 
 
 
11
  };
12
  };
 
8
  'db.name': process.env.DB_NAME,
9
  'db.slow_limit': Number(process.env.DB_SLOW_LIMIT) || 500, // ms
10
  'db.ssl_enabled': process.env.DB_SSL_ENABLED,
11
+ // payment config
12
+ 'vnp_TmnCode': process.env.VNP_TMNCODE,
13
+ 'vnp_HashSecret': process.env.VNP_HASHSECRET,
14
+ 'vnp_Url': process.env.VNP_URL,
15
+ 'vnp_ReturnUrl': process.env.VNP_RETURNURL,
16
  };
17
  };
backend/src/entities/branch-menu.entity.ts CHANGED
@@ -28,6 +28,9 @@ export class BranchMenuEntity extends BaseEntity {
28
  @Column({ default: true })
29
  is_open: boolean;
30
 
 
 
 
31
  @ManyToOne(() => BranchEntity, (a) => a.menu_items)
32
  @JoinColumn({ name: 'branch_id' })
33
  branch: Relation<BranchEntity>;
 
28
  @Column({ default: true })
29
  is_open: boolean;
30
 
31
+ @Column({ default: 0 })
32
+ sold_count: number;
33
+
34
  @ManyToOne(() => BranchEntity, (a) => a.menu_items)
35
  @JoinColumn({ name: 'branch_id' })
36
  branch: Relation<BranchEntity>;
backend/src/entities/branch.entity.ts CHANGED
@@ -2,6 +2,7 @@ import {
2
  BaseEntity,
3
  Column,
4
  CreateDateColumn,
 
5
  Entity,
6
  ManyToOne,
7
  OneToMany,
@@ -11,19 +12,23 @@ import {
11
  } from 'typeorm';
12
  import { UserEntity } from './user.entity.js';
13
  import { BranchMenuEntity } from './branch-menu.entity.js';
 
14
 
15
  @Entity('branches')
16
  export class BranchEntity extends BaseEntity {
17
  @PrimaryColumn()
18
  id: string;
19
 
20
- @Column()
21
  name: string;
22
 
23
- @Column()
 
 
 
24
  location: string;
25
 
26
- @Column()
27
  phone_number: string;
28
 
29
  @ManyToOne(() => UserEntity, (user) => user.branches)
@@ -32,6 +37,12 @@ export class BranchEntity extends BaseEntity {
32
  @OneToMany(() => BranchMenuEntity, (a) => a.branch)
33
  menu_items: Relation<BranchMenuEntity>[];
34
 
 
 
 
35
  @CreateDateColumn()
36
  create_at: Date;
 
 
 
37
  }
 
2
  BaseEntity,
3
  Column,
4
  CreateDateColumn,
5
+ DeleteDateColumn,
6
  Entity,
7
  ManyToOne,
8
  OneToMany,
 
12
  } from 'typeorm';
13
  import { UserEntity } from './user.entity.js';
14
  import { BranchMenuEntity } from './branch-menu.entity.js';
15
+ import { ReceiptEntity } from './receipt.entity.js';
16
 
17
  @Entity('branches')
18
  export class BranchEntity extends BaseEntity {
19
  @PrimaryColumn()
20
  id: string;
21
 
22
+ @Column({ nullable: true })
23
  name: string;
24
 
25
+ @Column({ nullable: true })
26
+ image_url: string;
27
+
28
+ @Column({ nullable: true })
29
  location: string;
30
 
31
+ @Column({ nullable: true })
32
  phone_number: string;
33
 
34
  @ManyToOne(() => UserEntity, (user) => user.branches)
 
37
  @OneToMany(() => BranchMenuEntity, (a) => a.branch)
38
  menu_items: Relation<BranchMenuEntity>[];
39
 
40
+ @OneToMany(() => ReceiptEntity, (a) => a.branch)
41
+ receipts: Relation<ReceiptEntity>[];
42
+
43
  @CreateDateColumn()
44
  create_at: Date;
45
+
46
+ @DeleteDateColumn()
47
+ delete_at: Date;
48
  }
backend/src/entities/feed.entity.ts CHANGED
@@ -1,4 +1,11 @@
1
- import { Entity, Column, BaseEntity, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
 
 
 
 
 
 
 
2
 
3
  @Entity('feeds')
4
  export class FeedEntity extends BaseEntity {
@@ -19,4 +26,7 @@ export class FeedEntity extends BaseEntity {
19
 
20
  @CreateDateColumn()
21
  create_at: Date;
 
 
 
22
  }
 
1
+ import {
2
+ Entity,
3
+ Column,
4
+ BaseEntity,
5
+ PrimaryGeneratedColumn,
6
+ CreateDateColumn,
7
+ DeleteDateColumn,
8
+ } from 'typeorm';
9
 
10
  @Entity('feeds')
11
  export class FeedEntity extends BaseEntity {
 
26
 
27
  @CreateDateColumn()
28
  create_at: Date;
29
+
30
+ @DeleteDateColumn()
31
+ delete_at: Date;
32
  }
backend/src/entities/menu-item.entity.ts CHANGED
@@ -2,6 +2,7 @@ import {
2
  BaseEntity,
3
  Column,
4
  CreateDateColumn,
 
5
  Entity,
6
  OneToMany,
7
  PrimaryColumn,
@@ -21,8 +22,8 @@ export class MenuItemEntity extends BaseEntity {
21
  @Column({ nullable: true })
22
  image_url: string;
23
 
24
- @Column({ type: 'enum', enum: MenuItemType, default: 'khac' })
25
- item_type: MenuItemType;
26
 
27
  @Column()
28
  description: string;
@@ -35,4 +36,7 @@ export class MenuItemEntity extends BaseEntity {
35
 
36
  @CreateDateColumn()
37
  create_at: Date;
 
 
 
38
  }
 
2
  BaseEntity,
3
  Column,
4
  CreateDateColumn,
5
+ DeleteDateColumn,
6
  Entity,
7
  OneToMany,
8
  PrimaryColumn,
 
22
  @Column({ nullable: true })
23
  image_url: string;
24
 
25
+ @Column({ default: 0 })
26
+ item_type: number;
27
 
28
  @Column()
29
  description: string;
 
36
 
37
  @CreateDateColumn()
38
  create_at: Date;
39
+
40
+ @DeleteDateColumn()
41
+ delete_at: Date;
42
  }
backend/src/entities/order.entity.ts CHANGED
@@ -2,6 +2,7 @@ import {
2
  BaseEntity,
3
  Column,
4
  CreateDateColumn,
 
5
  Entity,
6
  JoinColumn,
7
  ManyToOne,
@@ -9,6 +10,7 @@ import {
9
  OneToOne,
10
  PrimaryGeneratedColumn,
11
  Relation,
 
12
  } from 'typeorm';
13
  import { BranchEntity } from './branch.entity.js';
14
  import { UserEntity } from './user.entity.js';
@@ -46,17 +48,20 @@ export class OrderEntity extends BaseEntity {
46
  @Column({ nullable: true })
47
  table_number: number;
48
 
 
 
 
 
 
 
49
  @Column()
50
  total_value: number;
51
 
52
- @CreateDateColumn()
53
- create_at: Date;
54
-
55
- @Column({ type: 'enum', enum: OrderType, default: OrderType.ONLINE })
56
- order_type: OrderType;
57
 
58
- @Column({ type: 'enum', enum: OrderStatus, default: OrderStatus.PENDING })
59
- order_status: OrderStatus;
60
 
61
  @OneToMany(() => OrderItemEntity, (a) => a.order)
62
  order_items: Relation<OrderItemEntity>[];
@@ -67,4 +72,13 @@ export class OrderEntity extends BaseEntity {
67
  @OneToOne(() => PaymentEntity, (a) => a.order)
68
  @JoinColumn({ name: 'payment_id' })
69
  payment: Relation<PaymentEntity>;
 
 
 
 
 
 
 
 
 
70
  }
 
2
  BaseEntity,
3
  Column,
4
  CreateDateColumn,
5
+ DeleteDateColumn,
6
  Entity,
7
  JoinColumn,
8
  ManyToOne,
 
10
  OneToOne,
11
  PrimaryGeneratedColumn,
12
  Relation,
13
+ UpdateDateColumn,
14
  } from 'typeorm';
15
  import { BranchEntity } from './branch.entity.js';
16
  import { UserEntity } from './user.entity.js';
 
48
  @Column({ nullable: true })
49
  table_number: number;
50
 
51
+ @Column({ nullable: true })
52
+ note: string;
53
+
54
+ @Column({ nullable: true })
55
+ rating: number;
56
+
57
  @Column()
58
  total_value: number;
59
 
60
+ @Column({ default: 0 })
61
+ order_type: number;
 
 
 
62
 
63
+ @Column({ default: 0 })
64
+ order_status: number;
65
 
66
  @OneToMany(() => OrderItemEntity, (a) => a.order)
67
  order_items: Relation<OrderItemEntity>[];
 
72
  @OneToOne(() => PaymentEntity, (a) => a.order)
73
  @JoinColumn({ name: 'payment_id' })
74
  payment: Relation<PaymentEntity>;
75
+
76
+ @CreateDateColumn()
77
+ created_at: Date;
78
+
79
+ @UpdateDateColumn()
80
+ updated_at: Date;
81
+
82
+ @DeleteDateColumn()
83
+ deleted_at: Date;
84
  }
backend/src/entities/payment.entity.ts CHANGED
@@ -2,15 +2,11 @@ import {
2
  BaseEntity,
3
  Column,
4
  Entity,
5
- JoinColumn,
6
- ManyToOne,
7
  OneToOne,
8
  PrimaryGeneratedColumn,
9
  Relation,
10
  } from 'typeorm';
11
- import { BranchMenuEntity } from './branch-menu.entity.js';
12
  import { OrderEntity } from './order.entity.js';
13
- import { PaymentMethod } from '../common/enums/PaymentMethod.enum.js';
14
 
15
  @Entity('payments')
16
  export class PaymentEntity extends BaseEntity {
@@ -20,9 +16,33 @@ export class PaymentEntity extends BaseEntity {
20
  @OneToOne(() => OrderEntity, (a) => a.payment)
21
  order: Relation<OrderEntity>;
22
 
23
- @Column({ type: 'enum', enum: PaymentMethod, default: PaymentMethod.CASH })
24
- payment_method: PaymentMethod; // E.g., 'Cash', 'Credit Card', 'Online Payment'
25
 
26
  @Column()
27
- value: number;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
 
2
  BaseEntity,
3
  Column,
4
  Entity,
 
 
5
  OneToOne,
6
  PrimaryGeneratedColumn,
7
  Relation,
8
  } from 'typeorm';
 
9
  import { OrderEntity } from './order.entity.js';
 
10
 
11
  @Entity('payments')
12
  export class PaymentEntity extends BaseEntity {
 
16
  @OneToOne(() => OrderEntity, (a) => a.payment)
17
  order: Relation<OrderEntity>;
18
 
19
+ @Column({ default: 0 })
20
+ payment_method: number;
21
 
22
  @Column()
23
+ vnp_amount: number
24
+
25
+ @Column({ nullable: true })
26
+ vnp_bank_code: string
27
+
28
+ @Column({ nullable: true })
29
+ vnp_bank_tran_no:string
30
+
31
+ @Column({ default: 0})
32
+ vnp_card_type: number
33
+
34
+ @Column({ nullable: true })
35
+ vnp_order_info: string //Nội dung giao dịch
36
+
37
+ @Column({ nullable: true })
38
+ vnp_paydate: string
39
+
40
+ @Column({ nullable: true })
41
+ vnp_response_code: number
42
+
43
+ @Column({ nullable: true })
44
+ vnp_transaction_no: string
45
+
46
+ @Column({ nullable: true })
47
+ vnp_transaction_status: number
48
  }
backend/src/entities/receipt.entity.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ BaseEntity,
3
+ Column,
4
+ CreateDateColumn,
5
+ Entity,
6
+ JoinColumn,
7
+ ManyToOne,
8
+ PrimaryGeneratedColumn,
9
+ Relation,
10
+ } from 'typeorm';
11
+ import { BranchEntity } from './branch.entity.js';
12
+ import { UserEntity } from './user.entity.js';
13
+
14
+ @Entity('receipts')
15
+ export class ReceiptEntity extends BaseEntity {
16
+ @PrimaryGeneratedColumn()
17
+ id: number;
18
+
19
+ @Column()
20
+ branch_id: string;
21
+
22
+ @ManyToOne(() => BranchEntity, (a) => a.receipts)
23
+ @JoinColumn({ name: 'branch_id' })
24
+ branch: Relation<BranchEntity>;
25
+
26
+ @Column({ default: 0 })
27
+ income: number;
28
+
29
+ @Column({ default: 0 })
30
+ spend: number;
31
+
32
+ @Column({ nullable: true })
33
+ description: string;
34
+
35
+ @Column({ default: 0 })
36
+ type: number;
37
+
38
+ @Column({ default: 0 })
39
+ sub_type: number;
40
+
41
+ @Column({ nullable: true })
42
+ sender_id: string;
43
+
44
+ @Column({ nullable: true })
45
+ receiver_id: string;
46
+
47
+ @ManyToOne(() => UserEntity, (a) => a.out_receipts)
48
+ @JoinColumn({ name: 'sender_id' })
49
+ sender: Relation<UserEntity>;
50
+
51
+ @ManyToOne(() => UserEntity, (a) => a.in_receipts)
52
+ @JoinColumn({ name: 'receiver_id' })
53
+ receiver: Relation<UserEntity>;
54
+
55
+ @CreateDateColumn()
56
+ created_at: Date;
57
+ }
backend/src/entities/user.entity.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
  import { BranchEntity } from './branch.entity.js';
13
  import { IsOptional } from 'class-validator';
14
  import { Role } from '../common/enums/role.enum.js';
 
15
 
16
  @Entity('users')
17
  export class UserEntity extends BaseEntity {
@@ -37,19 +38,22 @@ export class UserEntity extends BaseEntity {
37
 
38
  @Column({ type: 'enum', enum: Role, default: 'CUSTOMER' })
39
  role: Role;
40
-
41
  @Column()
42
  hash_password: string;
43
 
44
- @IsOptional()
45
  @Column({ default: true })
46
  is_valid: boolean;
47
 
48
- @IsOptional()
49
  @CreateDateColumn()
50
  create_at: Date;
51
 
52
- @IsOptional()
53
  @OneToMany(() => BranchEntity, (branch) => branch.owner)
54
  branches: Relation<BranchEntity>[];
 
 
 
 
 
 
55
  }
 
12
  import { BranchEntity } from './branch.entity.js';
13
  import { IsOptional } from 'class-validator';
14
  import { Role } from '../common/enums/role.enum.js';
15
+ import { ReceiptEntity } from './receipt.entity.js';
16
 
17
  @Entity('users')
18
  export class UserEntity extends BaseEntity {
 
38
 
39
  @Column({ type: 'enum', enum: Role, default: 'CUSTOMER' })
40
  role: Role;
41
+
42
  @Column()
43
  hash_password: string;
44
 
 
45
  @Column({ default: true })
46
  is_valid: boolean;
47
 
 
48
  @CreateDateColumn()
49
  create_at: Date;
50
 
 
51
  @OneToMany(() => BranchEntity, (branch) => branch.owner)
52
  branches: Relation<BranchEntity>[];
53
+
54
+ @OneToMany(() => ReceiptEntity, (receipt) => receipt.sender)
55
+ in_receipts: Relation<ReceiptEntity>[];
56
+
57
+ @OneToMany(() => ReceiptEntity, (receipt) => receipt.receiver)
58
+ out_receipts: Relation<ReceiptEntity>[];
59
  }
backend/src/migrations/1730547520878-AddReceipt.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddReceipt1730547520878 implements MigrationInterface {
4
+ name = 'AddReceipt1730547520878'
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(`CREATE TABLE "receipts" ("id" SERIAL NOT NULL, "branch_id" character varying NOT NULL, "income" integer NOT NULL DEFAULT '0', "spend" integer NOT NULL DEFAULT '0', "description" character varying, "type" integer NOT NULL DEFAULT '0', "sub_type" integer NOT NULL DEFAULT '0', "sender_id" uuid, "receiver_id" uuid, "created_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_5e8182d7c29e023da6e1ff33bfe" PRIMARY KEY ("id"))`);
8
+ await queryRunner.query(`ALTER TABLE "branches" ADD "image_url" character varying`);
9
+ await queryRunner.query(`ALTER TABLE "branch_menu" ADD "sold_count" integer NOT NULL DEFAULT '0'`);
10
+ await queryRunner.query(`ALTER TABLE "branches" ALTER COLUMN "name" DROP NOT NULL`);
11
+ await queryRunner.query(`ALTER TABLE "branches" ALTER COLUMN "location" DROP NOT NULL`);
12
+ await queryRunner.query(`ALTER TABLE "branches" ALTER COLUMN "phone_number" DROP NOT NULL`);
13
+ await queryRunner.query(`ALTER TABLE "receipts" ADD CONSTRAINT "FK_82e9dee911c0e7393154d1d98ad" FOREIGN KEY ("branch_id") REFERENCES "branches"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
14
+ await queryRunner.query(`ALTER TABLE "receipts" ADD CONSTRAINT "FK_eda4c4e486a25beef4dc82a41d5" FOREIGN KEY ("sender_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
15
+ await queryRunner.query(`ALTER TABLE "receipts" ADD CONSTRAINT "FK_366c3d3cf125da97552f40001a1" FOREIGN KEY ("receiver_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
16
+ }
17
+
18
+ public async down(queryRunner: QueryRunner): Promise<void> {
19
+ await queryRunner.query(`ALTER TABLE "receipts" DROP CONSTRAINT "FK_366c3d3cf125da97552f40001a1"`);
20
+ await queryRunner.query(`ALTER TABLE "receipts" DROP CONSTRAINT "FK_eda4c4e486a25beef4dc82a41d5"`);
21
+ await queryRunner.query(`ALTER TABLE "receipts" DROP CONSTRAINT "FK_82e9dee911c0e7393154d1d98ad"`);
22
+ await queryRunner.query(`ALTER TABLE "branches" ALTER COLUMN "phone_number" SET NOT NULL`);
23
+ await queryRunner.query(`ALTER TABLE "branches" ALTER COLUMN "location" SET NOT NULL`);
24
+ await queryRunner.query(`ALTER TABLE "branches" ALTER COLUMN "name" SET NOT NULL`);
25
+ await queryRunner.query(`ALTER TABLE "branch_menu" DROP COLUMN "sold_count"`);
26
+ await queryRunner.query(`ALTER TABLE "branches" DROP COLUMN "image_url"`);
27
+ await queryRunner.query(`DROP TABLE "receipts"`);
28
+ }
29
+
30
+ }
backend/src/migrations/1730549959767-RemoveEnums.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class RemoveEnums1730549959767 implements MigrationInterface {
4
+ name = 'RemoveEnums1730549959767'
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(`ALTER TABLE "menu_items" DROP COLUMN "item_type"`);
8
+ await queryRunner.query(`DROP TYPE "public"."menu_items_item_type_enum"`);
9
+ await queryRunner.query(`ALTER TABLE "menu_items" ADD "item_type" integer NOT NULL DEFAULT '0'`);
10
+ await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "payment_method"`);
11
+ await queryRunner.query(`DROP TYPE "public"."payments_payment_method_enum"`);
12
+ await queryRunner.query(`ALTER TABLE "payments" ADD "payment_method" integer NOT NULL DEFAULT '0'`);
13
+ await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "order_type"`);
14
+ await queryRunner.query(`DROP TYPE "public"."orders_order_type_enum"`);
15
+ await queryRunner.query(`ALTER TABLE "orders" ADD "order_type" integer NOT NULL DEFAULT '0'`);
16
+ await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "order_status"`);
17
+ await queryRunner.query(`DROP TYPE "public"."orders_order_status_enum"`);
18
+ await queryRunner.query(`ALTER TABLE "orders" ADD "order_status" integer NOT NULL DEFAULT '0'`);
19
+ }
20
+
21
+ public async down(queryRunner: QueryRunner): Promise<void> {
22
+ await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "order_status"`);
23
+ await queryRunner.query(`CREATE TYPE "public"."orders_order_status_enum" AS ENUM('pending', 'confirmed', 'preparing', 'delivering', 'done')`);
24
+ await queryRunner.query(`ALTER TABLE "orders" ADD "order_status" "public"."orders_order_status_enum" NOT NULL DEFAULT 'pending'`);
25
+ await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "order_type"`);
26
+ await queryRunner.query(`CREATE TYPE "public"."orders_order_type_enum" AS ENUM('take_away', 'offline', 'online')`);
27
+ await queryRunner.query(`ALTER TABLE "orders" ADD "order_type" "public"."orders_order_type_enum" NOT NULL DEFAULT 'online'`);
28
+ await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "payment_method"`);
29
+ await queryRunner.query(`CREATE TYPE "public"."payments_payment_method_enum" AS ENUM('cash', 'card', 'online_payment')`);
30
+ await queryRunner.query(`ALTER TABLE "payments" ADD "payment_method" "public"."payments_payment_method_enum" NOT NULL DEFAULT 'cash'`);
31
+ await queryRunner.query(`ALTER TABLE "menu_items" DROP COLUMN "item_type"`);
32
+ await queryRunner.query(`CREATE TYPE "public"."menu_items_item_type_enum" AS ENUM('monchinh', 'trangmieng', 'giaikhat', 'khac')`);
33
+ await queryRunner.query(`ALTER TABLE "menu_items" ADD "item_type" "public"."menu_items_item_type_enum" NOT NULL DEFAULT 'khac'`);
34
+ }
35
+
36
+ }
backend/src/migrations/1730651201156-modify_payment.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class ModifyPayment1730651201156 implements MigrationInterface {
4
+ name = 'ModifyPayment1730651201156'
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "value"`);
8
+ await queryRunner.query(`ALTER TABLE "payments" ADD "vnp_amount" integer NOT NULL`);
9
+ await queryRunner.query(`ALTER TABLE "payments" ADD "vnp_bank_code" character varying`);
10
+ await queryRunner.query(`ALTER TABLE "payments" ADD "vnp_bank_tran_no" character varying`);
11
+ await queryRunner.query(`ALTER TABLE "payments" ADD "vnp_card_type" integer NOT NULL DEFAULT '0'`);
12
+ await queryRunner.query(`ALTER TABLE "payments" ADD "vnp_order_info" character varying`);
13
+ await queryRunner.query(`ALTER TABLE "payments" ADD "vnp_paydate" character varying`);
14
+ await queryRunner.query(`ALTER TABLE "payments" ADD "vnp_response_code" integer`);
15
+ await queryRunner.query(`ALTER TABLE "payments" ADD "vnp_transaction_no" character varying`);
16
+ await queryRunner.query(`ALTER TABLE "payments" ADD "vnp_transaction_status" integer`);
17
+ }
18
+
19
+ public async down(queryRunner: QueryRunner): Promise<void> {
20
+ await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "vnp_transaction_status"`);
21
+ await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "vnp_transaction_no"`);
22
+ await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "vnp_response_code"`);
23
+ await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "vnp_paydate"`);
24
+ await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "vnp_order_info"`);
25
+ await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "vnp_card_type"`);
26
+ await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "vnp_bank_tran_no"`);
27
+ await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "vnp_bank_code"`);
28
+ await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "vnp_amount"`);
29
+ await queryRunner.query(`ALTER TABLE "payments" ADD "value" integer NOT NULL`);
30
+ }
31
+
32
+ }
backend/src/migrations/1730865796585-AddBranchAndOrderFields.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddBranchAndOrderFields1730865796585 implements MigrationInterface {
4
+ name = 'AddBranchAndOrderFields1730865796585'
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(`ALTER TABLE "feeds" ADD "delete_at" TIMESTAMP`);
8
+ await queryRunner.query(`ALTER TABLE "menu_items" ADD "delete_at" TIMESTAMP`);
9
+ await queryRunner.query(`ALTER TABLE "branches" ADD "delete_at" TIMESTAMP`);
10
+ await queryRunner.query(`ALTER TABLE "orders" ADD "note" character varying`);
11
+ await queryRunner.query(`ALTER TABLE "orders" ADD "rating" integer`);
12
+ }
13
+
14
+ public async down(queryRunner: QueryRunner): Promise<void> {
15
+ await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "rating"`);
16
+ await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "note"`);
17
+ await queryRunner.query(`ALTER TABLE "branches" DROP COLUMN "delete_at"`);
18
+ await queryRunner.query(`ALTER TABLE "menu_items" DROP COLUMN "delete_at"`);
19
+ await queryRunner.query(`ALTER TABLE "feeds" DROP COLUMN "delete_at"`);
20
+ }
21
+
22
+ }
backend/src/migrations/1730895089788-UpdateOrderFieldTime.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MigrationInterface, QueryRunner } from 'typeorm';
2
+
3
+ export class UpdateOrderFieldTime1730895089788 implements MigrationInterface {
4
+ name = 'UpdateOrderFieldTime1730895089788';
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "create_at"`);
8
+ await queryRunner.query(
9
+ `ALTER TABLE "orders" ADD "created_at" TIMESTAMP NOT NULL DEFAULT now()`,
10
+ );
11
+ await queryRunner.query(
12
+ `ALTER TABLE "orders" ADD "updated_at" TIMESTAMP NOT NULL DEFAULT now()`,
13
+ );
14
+ await queryRunner.query(`ALTER TABLE "orders" ADD "deleted_at" TIMESTAMP`);
15
+ }
16
+
17
+ public async down(queryRunner: QueryRunner): Promise<void> {
18
+ await queryRunner.query(
19
+ `ALTER TABLE "orders" ADD "create_at" TIMESTAMP NOT NULL DEFAULT now()`,
20
+ );
21
+ await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "deleted_at"`);
22
+ await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "updated_at"`);
23
+ await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "created_at"`);
24
+ }
25
+ }
backend/src/modules/authentication/authentication.service.ts CHANGED
@@ -28,7 +28,7 @@ export class AuthenticationService {
28
  if (!compare) {
29
  throw new UnauthorizedException("Wrong password");
30
  }
31
- const payload = { sub: user.id, username: user.full_name, roles: user.role};
32
  return {
33
  access_token: await this.jwtService.signAsync(payload),
34
  };
 
28
  if (!compare) {
29
  throw new UnauthorizedException("Wrong password");
30
  }
31
+ const payload = { sub: user.id, username: user.full_name, role: user.role};
32
  return {
33
  access_token: await this.jwtService.signAsync(payload),
34
  };
backend/src/modules/authentication/authorization/roles.guard.ts CHANGED
@@ -17,6 +17,6 @@ export class RolesGuard implements CanActivate {
17
  return true;
18
  }
19
  const { user } = context.switchToHttp().getRequest();
20
- return requiredRoles.some((role) => user.roles.includes(role));
21
  }
22
  }
 
17
  return true;
18
  }
19
  const { user } = context.switchToHttp().getRequest();
20
+ return requiredRoles.some((role) => user.role.includes(role));
21
  }
22
  }
backend/src/modules/branch-menus/branch-menus.service.ts CHANGED
@@ -4,7 +4,12 @@ import { UpdateBranchMenuDto } from './dto/update-branch-menu.dto.js';
4
  import { BranchService } from '../branch/branch.service.js';
5
  import { MenuItemService } from '../menu-item/menu-item.service.js';
6
  import { BranchMenuEntity } from '../../entities/branch-menu.entity.js';
7
- import { paginate, PaginateConfig, PaginateQuery } from 'nestjs-paginate';
 
 
 
 
 
8
  import { isUUID } from 'class-validator';
9
 
10
  @Injectable()
@@ -36,12 +41,18 @@ export class BranchMenusService {
36
  const paginateConfig: PaginateConfig<BranchMenuEntity> = {
37
  sortableColumns: ['id', 'branch_id', 'menu_id', 'description'],
38
  nullSort: 'last',
39
- defaultSortBy: [['id', 'DESC']],
40
  searchableColumns: ['description'],
41
  filterableColumns: {
42
- // price: [],
43
- // item_type: [FilterOperator.EQ],
 
 
 
 
 
44
  },
 
45
  };
46
  return paginate(
47
  query,
 
4
  import { BranchService } from '../branch/branch.service.js';
5
  import { MenuItemService } from '../menu-item/menu-item.service.js';
6
  import { BranchMenuEntity } from '../../entities/branch-menu.entity.js';
7
+ import {
8
+ FilterOperator,
9
+ paginate,
10
+ PaginateConfig,
11
+ PaginateQuery,
12
+ } from 'nestjs-paginate';
13
  import { isUUID } from 'class-validator';
14
 
15
  @Injectable()
 
41
  const paginateConfig: PaginateConfig<BranchMenuEntity> = {
42
  sortableColumns: ['id', 'branch_id', 'menu_id', 'description'],
43
  nullSort: 'last',
44
+ defaultSortBy: [['menu_item.item_type', 'ASC']],
45
  searchableColumns: ['description'],
46
  filterableColumns: {
47
+ 'menu_item.price': [
48
+ FilterOperator.LT,
49
+ FilterOperator.LTE,
50
+ FilterOperator.GT,
51
+ FilterOperator.GTE,
52
+ ],
53
+ 'menu_item.item_type': [FilterOperator.EQ, FilterOperator.IN],
54
  },
55
+ relations: ['menu_item'],
56
  };
57
  return paginate(
58
  query,
backend/src/modules/branch/branch.controller.ts CHANGED
@@ -22,6 +22,11 @@ export class BranchController {
22
  return this.branchService.create(createBranchDto);
23
  }
24
 
 
 
 
 
 
25
  @Get()
26
  async findAll() {
27
  return this.branchService.findAll();
@@ -42,7 +47,7 @@ export class BranchController {
42
 
43
  @Delete(':id')
44
  async remove(@Param('id') id: string) {
45
- return this.branchService.remove(id);
46
  }
47
 
48
  @Post(':id/menu-items')
 
22
  return this.branchService.create(createBranchDto);
23
  }
24
 
25
+ @Post(':id/restore')
26
+ restore(@Param('id') id: string) {
27
+ return this.branchService.restore(id);
28
+ }
29
+
30
  @Get()
31
  async findAll() {
32
  return this.branchService.findAll();
 
47
 
48
  @Delete(':id')
49
  async remove(@Param('id') id: string) {
50
+ return this.branchService.softRemove(id);
51
  }
52
 
53
  @Post(':id/menu-items')
backend/src/modules/branch/branch.service.ts CHANGED
@@ -46,8 +46,14 @@ export class BranchService {
46
  return await branch.save();
47
  }
48
 
49
- async remove(id: string) {
50
- let menuItem = await this.getBranchOrError(id);
51
- return await menuItem.remove();
 
 
 
 
 
 
52
  }
53
  }
 
46
  return await branch.save();
47
  }
48
 
49
+ async softRemove(id: string) {
50
+ let branch = await this.getBranchOrError(id);
51
+ return await branch.softRemove();
52
+ }
53
+
54
+ async restore(id: string) {
55
+ let result = await BranchEntity.getRepository().restore(id);
56
+ let branch = await BranchEntity.findOneBy({ id });
57
+ return branch;
58
  }
59
  }
backend/src/modules/branch/dto/create-branch.dto.ts CHANGED
@@ -7,6 +7,9 @@ export class CreateBranchDto {
7
  @IsString()
8
  name: string;
9
 
 
 
 
10
  @IsString()
11
  location: string;
12
 
 
7
  @IsString()
8
  name: string;
9
 
10
+ @IsString()
11
+ image_url: string;
12
+
13
  @IsString()
14
  location: string;
15
 
backend/src/modules/branch/dto/update-branch.dto.ts CHANGED
@@ -5,6 +5,10 @@ export class UpdateBranchDto {
5
  @IsOptional()
6
  name?: string;
7
 
 
 
 
 
8
  @IsString()
9
  @IsOptional()
10
  location?: string;
 
5
  @IsOptional()
6
  name?: string;
7
 
8
+ @IsString()
9
+ @IsOptional()
10
+ image_url?: string;
11
+
12
  @IsString()
13
  @IsOptional()
14
  location?: string;
backend/src/modules/menu-item/dto/create-menu-item.dto.ts CHANGED
@@ -10,9 +10,9 @@ export class CreateMenuItemDto {
10
  @IsUrl()
11
  image_url: string;
12
 
13
- @IsString()
14
  @IsOptional()
15
- item_group_id?: string;
16
 
17
  @IsString()
18
  @IsOptional()
 
10
  @IsUrl()
11
  image_url: string;
12
 
13
+ @IsNumber()
14
  @IsOptional()
15
+ item_type?: number;
16
 
17
  @IsString()
18
  @IsOptional()
backend/src/modules/menu-item/dto/update-menu-item.dto.ts CHANGED
@@ -9,9 +9,9 @@ export class UpdateMenuItemDto {
9
  @IsOptional()
10
  image_url: string;
11
 
12
- @IsString()
13
  @IsOptional()
14
- item_group_id?: string;
15
 
16
  @IsString()
17
  @IsOptional()
 
9
  @IsOptional()
10
  image_url: string;
11
 
12
+ @IsNumber()
13
  @IsOptional()
14
+ item_type?: number;
15
 
16
  @IsString()
17
  @IsOptional()
backend/src/modules/order/dto/create-order.dto.ts CHANGED
@@ -16,8 +16,12 @@ export class CreateOrderDto {
16
  @IsNumber()
17
  table_number?: number;
18
 
19
- @IsEnum(OrderType)
20
- order_type: OrderType;
 
 
 
 
21
 
22
  @IsArray()
23
  @ValidateNested()
 
16
  @IsNumber()
17
  table_number?: number;
18
 
19
+ @IsOptional()
20
+ @IsString()
21
+ note?: string;
22
+
23
+ @IsNumber()
24
+ order_type: number;
25
 
26
  @IsArray()
27
  @ValidateNested()
backend/src/modules/order/dto/update-order.dto.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsArray, IsNumber, IsOptional, ValidateNested } from 'class-validator';
2
+ import { OrderItemsDto } from './order-items.dto.js';
3
+ import { Type } from 'class-transformer';
4
+
5
+ export class UpdateOrderDto {
6
+ @IsOptional()
7
+ @IsNumber()
8
+ table_number?: number;
9
+
10
+ @IsArray()
11
+ @ValidateNested()
12
+ @Type(() => OrderItemsDto)
13
+ order_items: OrderItemsDto[];
14
+ }
backend/src/modules/order/order.controller.ts CHANGED
@@ -11,9 +11,12 @@ import {
11
  import { OrderService } from './order.service.js';
12
  import { CreateOrderDto } from './dto/create-order.dto.js';
13
  import { Role } from '../../common/enums/role.enum.js';
 
 
 
14
 
15
  @Controller('branchs/:branchId/orders')
16
- export class OrderController {
17
  constructor(private readonly orderService: OrderService) {}
18
 
19
  @Post()
@@ -23,15 +26,16 @@ export class OrderController {
23
  @Body() createOrderDto: CreateOrderDto,
24
  ) {
25
  const userId = req['user'].sub;
26
- const role = req['user'].roles;
27
  console.log(req['user']);
28
- if (role == Role.CUSTOMER)
 
29
  return this.orderService.createFromCustomer(
30
  branchId,
31
  userId,
32
  createOrderDto,
33
  );
34
- else
35
  return this.orderService.createFromStaff(
36
  branchId,
37
  userId,
@@ -39,28 +43,50 @@ export class OrderController {
39
  );
40
  }
41
 
42
- @Get()
43
- async findAll(@Req() req: Request) {
 
 
 
 
 
44
  const userId = req['user'].sub;
45
  console.log(req['user']);
46
- return this.orderService.findAll();
47
  }
48
 
49
  @Get(':id')
50
  async findOne(@Param('id') id: string) {
51
  return this.orderService.findOne(+id);
52
  }
53
-
54
- // @Patch(':id')
55
- // async update(
56
- // @Param('id') id: string,
57
- // @Body() updateOrderDto: UpdateOrderDto,
58
- // ) {
59
- // return this.orderService.update(+id, updateOrderDto);
60
- // }
61
 
62
  @Delete(':id')
63
  remove(@Param('id') id: string) {
64
  return this.orderService.remove(+id);
65
  }
66
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  import { OrderService } from './order.service.js';
12
  import { CreateOrderDto } from './dto/create-order.dto.js';
13
  import { Role } from '../../common/enums/role.enum.js';
14
+ import { paginate, Paginate, PaginateQuery } from 'nestjs-paginate';
15
+ import { Roles } from '../authentication/authorization/roles.decorator.js';
16
+ import { UpdateOrderDto } from './dto/update-order.dto.js';
17
 
18
  @Controller('branchs/:branchId/orders')
19
+ export class BranchOrderController {
20
  constructor(private readonly orderService: OrderService) {}
21
 
22
  @Post()
 
26
  @Body() createOrderDto: CreateOrderDto,
27
  ) {
28
  const userId = req['user'].sub;
29
+ const role = req['user'].role;
30
  console.log(req['user']);
31
+ if (role === Role.CUSTOMER) {
32
+ console.log('customer');
33
  return this.orderService.createFromCustomer(
34
  branchId,
35
  userId,
36
  createOrderDto,
37
  );
38
+ } else
39
  return this.orderService.createFromStaff(
40
  branchId,
41
  userId,
 
43
  );
44
  }
45
 
46
+ @Get('history')
47
+ async findHistory(
48
+ @Req() req: Request,
49
+ @Param('branchId') branchId: string,
50
+ @Paginate() paginateQuery: PaginateQuery,
51
+ ) {
52
+ // order history of user.
53
  const userId = req['user'].sub;
54
  console.log(req['user']);
55
+ return this.orderService.findHistory(paginateQuery, userId, branchId);
56
  }
57
 
58
  @Get(':id')
59
  async findOne(@Param('id') id: string) {
60
  return this.orderService.findOne(+id);
61
  }
62
+ @Patch(':id')
63
+ update(@Param('id') id: string, @Body() updateOrderDto: UpdateOrderDto) {
64
+ return this.orderService.updateOrder(+id, updateOrderDto);
65
+ }
 
 
 
 
66
 
67
  @Delete(':id')
68
  remove(@Param('id') id: string) {
69
  return this.orderService.remove(+id);
70
  }
71
  }
72
+
73
+ @Roles(Role.ADMIN, Role.STAFF, Role.BRANCH_MANAGER, Role.SHIPPER)
74
+ @Controller('orders')
75
+ export class OrderController {
76
+ constructor(private readonly orderService: OrderService) {}
77
+
78
+ @Get()
79
+ async findAll(@Paginate() paginateQuery: PaginateQuery) {
80
+ return this.orderService.findAll(paginateQuery);
81
+ }
82
+
83
+ @Get(':id')
84
+ async findOne(@Param('id') id: string) {
85
+ return this.orderService.findOne(+id);
86
+ }
87
+
88
+ @Patch(':id')
89
+ update(@Param('id') id: string, @Body() updateOrderDto: UpdateOrderDto) {
90
+ return this.orderService.updateOrder(+id, updateOrderDto);
91
+ }
92
+ }
backend/src/modules/order/order.module.ts CHANGED
@@ -1,12 +1,13 @@
1
  import { Module } from '@nestjs/common';
2
  import { OrderService } from './order.service.js';
3
- import { OrderController } from './order.controller.js';
4
  import { BranchModule } from '../branch/branch.module.js';
5
  import { BranchMenusModule } from '../branch-menus/branch-menus.module.js';
6
 
7
  @Module({
8
  imports: [BranchModule, BranchMenusModule],
9
- controllers: [OrderController],
10
  providers: [OrderService],
 
11
  })
12
  export class OrderModule {}
 
1
  import { Module } from '@nestjs/common';
2
  import { OrderService } from './order.service.js';
3
+ import { BranchOrderController, OrderController } from './order.controller.js';
4
  import { BranchModule } from '../branch/branch.module.js';
5
  import { BranchMenusModule } from '../branch-menus/branch-menus.module.js';
6
 
7
  @Module({
8
  imports: [BranchModule, BranchMenusModule],
9
+ controllers: [BranchOrderController, OrderController],
10
  providers: [OrderService],
11
+ exports: [OrderService],
12
  })
13
  export class OrderModule {}
backend/src/modules/order/order.service.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { BadRequestException, Injectable } from '@nestjs/common';
 
 
 
 
2
  import { CreateOrderDto } from './dto/create-order.dto.js';
3
  import { UserEntity } from '../../entities/user.entity.js';
4
  import { OrderEntity } from '../../entities/order.entity.js';
@@ -8,16 +12,27 @@ import { isUUID } from 'class-validator';
8
  import { OrderItemEntity } from '../../entities/order-item.entity.js';
9
  import { BranchMenuEntity } from '../../entities/branch-menu.entity.js';
10
  import { OrderStatus } from '../../common/enums/OrderStatus.enum.js';
 
 
 
 
 
 
 
 
11
 
12
  @Injectable()
13
  export class OrderService {
14
- constructor(private readonly branchService: BranchService) {}
 
 
 
15
  async createFromCustomer(
16
  branchId: string,
17
  userId: string,
18
  createOrderDto: CreateOrderDto,
19
  ) {
20
- console.log('??');
21
  if (createOrderDto.order_type != OrderType.ONLINE) {
22
  throw new BadRequestException('customer cannot create offline order');
23
  }
@@ -30,44 +45,60 @@ export class OrderService {
30
  if (!branch) {
31
  throw new BadRequestException('Branch not found');
32
  }
33
- const order = OrderEntity.create();
34
- order.branch = branch;
35
- order.customer = user;
36
- order.order_type = createOrderDto.order_type;
37
- order.order_status = OrderStatus.PENDING;
38
- order.total_value = 0;
39
- await order.save();
40
 
41
- let orderItems: OrderItemEntity[] = [];
42
- let totalValue = 0;
43
- for (const item of createOrderDto.order_items) {
44
- let branchMenu: BranchMenuEntity;
45
- if (!isUUID(item.menu_id)) {
46
- branchMenu = await BranchMenuEntity.findOne({
47
- where: { branch_id: branchId, menu_id: item.menu_id },
48
- relations: ['menu_item'],
49
- });
50
- } else {
51
- branchMenu = await BranchMenuEntity.findOne({
52
- where: { branch_id: branchId, id: item.menu_id },
53
- relations: ['menu_item'],
54
- });
55
- }
56
- if (!branchMenu) {
57
- throw new BadRequestException('Item not found in branch menu');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  }
59
- const orderItem = OrderItemEntity.create();
60
- orderItem.branch_menu = branchMenu;
61
- orderItem.price = branchMenu.menu_item.price;
62
- orderItem.quantity = item.quantity;
63
- orderItem.order_id = order.id;
64
- totalValue += orderItem.price * orderItem.quantity;
65
- orderItems.push(orderItem);
 
 
 
 
66
  }
67
- await order.save();
68
- order.total_value = totalValue;
69
- await OrderItemEntity.save(orderItems);
70
- return { ...order, order_items: orderItems };
71
  }
72
 
73
  async createFromStaff(
@@ -89,49 +120,150 @@ export class OrderService {
89
  order.table_number = createOrderDto.table_number;
90
  order.order_status = OrderStatus.PREPARING;
91
  order.total_value = 0;
92
- await order.save();
93
 
94
- let orderItems: OrderItemEntity[] = [];
95
- let totalValue = 0;
96
- for (const item of createOrderDto.order_items) {
97
- let branchMenu: BranchMenuEntity;
98
- if (!isUUID(item.menu_id)) {
99
- branchMenu = await BranchMenuEntity.findOne({
100
- where: { branch_id: branchId, menu_id: item.menu_id },
101
- relations: ['menu_item'],
102
- });
103
- } else {
104
- branchMenu = await BranchMenuEntity.findOne({
105
- where: { branch_id: branchId, id: item.menu_id },
106
- relations: ['menu_item'],
107
- });
108
- }
109
- if (!branchMenu) {
110
- throw new BadRequestException('Item not found in branch menu');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  }
112
- const orderItem = OrderItemEntity.create();
113
- orderItem.branch_menu = branchMenu;
114
- orderItem.price = branchMenu.menu_item.price;
115
- orderItem.quantity = item.quantity;
116
- orderItem.order_id = order.id;
117
- totalValue += orderItem.price * orderItem.quantity;
118
- orderItems.push(orderItem);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  await order.save();
121
- order.total_value = totalValue;
122
- await OrderItemEntity.save(orderItems);
123
- return { ...order, order_items: orderItems };
124
  }
125
 
126
- findAll() {
127
- return `This action returns all order`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  }
129
 
130
- findOne(id: number) {
131
- return `This action returns a #${id} order`;
132
  }
133
 
134
- remove(id: number) {
135
- return `This action removes a #${id} order`;
 
136
  }
137
  }
 
1
+ import {
2
+ BadRequestException,
3
+ Injectable,
4
+ NotFoundException,
5
+ } from '@nestjs/common';
6
  import { CreateOrderDto } from './dto/create-order.dto.js';
7
  import { UserEntity } from '../../entities/user.entity.js';
8
  import { OrderEntity } from '../../entities/order.entity.js';
 
12
  import { OrderItemEntity } from '../../entities/order-item.entity.js';
13
  import { BranchMenuEntity } from '../../entities/branch-menu.entity.js';
14
  import { OrderStatus } from '../../common/enums/OrderStatus.enum.js';
15
+ import {
16
+ FilterOperator,
17
+ paginate,
18
+ PaginateConfig,
19
+ PaginateQuery,
20
+ } from 'nestjs-paginate';
21
+ import { UpdateOrderDto } from './dto/update-order.dto.js';
22
+ import { DataSource } from 'typeorm';
23
 
24
  @Injectable()
25
  export class OrderService {
26
+ constructor(
27
+ private readonly branchService: BranchService,
28
+ private readonly datasource: DataSource,
29
+ ) {}
30
  async createFromCustomer(
31
  branchId: string,
32
  userId: string,
33
  createOrderDto: CreateOrderDto,
34
  ) {
35
+ // Chưa được update đầy đủ, còn thiếu thông tin thanh toán.
36
  if (createOrderDto.order_type != OrderType.ONLINE) {
37
  throw new BadRequestException('customer cannot create offline order');
38
  }
 
45
  if (!branch) {
46
  throw new BadRequestException('Branch not found');
47
  }
48
+ const queryRunner = this.datasource.createQueryRunner();
 
 
 
 
 
 
49
 
50
+ // Start a new transaction
51
+ await queryRunner.connect();
52
+ await queryRunner.startTransaction();
53
+
54
+ try {
55
+ console.log('Customer create order');
56
+ const order = OrderEntity.create();
57
+ order.branch = branch;
58
+ order.customer = user;
59
+ order.order_type = createOrderDto.order_type;
60
+ order.order_status = OrderStatus.PENDING;
61
+ order.total_value = 0;
62
+ await queryRunner.manager.save(order);
63
+
64
+ let orderItems: OrderItemEntity[] = [];
65
+ let totalValue = 0;
66
+ for (const item of createOrderDto.order_items) {
67
+ let branchMenu: BranchMenuEntity;
68
+ if (!isUUID(item.menu_id)) {
69
+ branchMenu = await BranchMenuEntity.findOne({
70
+ where: { branch_id: branchId, menu_id: item.menu_id },
71
+ relations: ['menu_item'],
72
+ });
73
+ } else {
74
+ branchMenu = await BranchMenuEntity.findOne({
75
+ where: { branch_id: branchId, id: item.menu_id },
76
+ relations: ['menu_item'],
77
+ });
78
+ }
79
+ if (!branchMenu) {
80
+ throw new BadRequestException('Item not found in branch menu');
81
+ }
82
+ const orderItem = OrderItemEntity.create();
83
+ orderItem.branch_menu = branchMenu;
84
+ orderItem.price = branchMenu.menu_item.price;
85
+ orderItem.quantity = item.quantity;
86
+ orderItem.order_id = order.id;
87
+ totalValue += orderItem.price * orderItem.quantity;
88
+ orderItems.push(orderItem);
89
  }
90
+ await queryRunner.manager.save(order);
91
+ order.total_value = totalValue;
92
+ await queryRunner.manager.save(orderItems);
93
+
94
+ await queryRunner.commitTransaction();
95
+ await queryRunner.release();
96
+ return { ...order, order_items: orderItems };
97
+ } catch (err) {
98
+ await queryRunner.rollbackTransaction();
99
+ await queryRunner.release();
100
+ throw err;
101
  }
 
 
 
 
102
  }
103
 
104
  async createFromStaff(
 
120
  order.table_number = createOrderDto.table_number;
121
  order.order_status = OrderStatus.PREPARING;
122
  order.total_value = 0;
 
123
 
124
+ const queryRunner = this.datasource.createQueryRunner();
125
+
126
+ // Start a new transaction
127
+ await queryRunner.connect();
128
+ await queryRunner.startTransaction();
129
+
130
+ try {
131
+ await queryRunner.manager.save(order);
132
+
133
+ let orderItems: OrderItemEntity[] = [];
134
+ let totalValue = 0;
135
+ for (const item of createOrderDto.order_items) {
136
+ let branchMenu: BranchMenuEntity;
137
+ if (!isUUID(item.menu_id)) {
138
+ branchMenu = await BranchMenuEntity.findOne({
139
+ where: { branch_id: branchId, menu_id: item.menu_id },
140
+ relations: ['menu_item'],
141
+ });
142
+ } else {
143
+ branchMenu = await BranchMenuEntity.findOne({
144
+ where: { branch_id: branchId, id: item.menu_id },
145
+ relations: ['menu_item'],
146
+ });
147
+ }
148
+ if (!branchMenu) {
149
+ throw new BadRequestException('Item not found in branch menu');
150
+ }
151
+ const orderItem = OrderItemEntity.create();
152
+ orderItem.branch_menu = branchMenu;
153
+ orderItem.price = branchMenu.menu_item.price;
154
+ orderItem.quantity = item.quantity;
155
+ orderItem.order_id = order.id;
156
+ totalValue += orderItem.price * orderItem.quantity;
157
+ orderItems.push(orderItem);
158
  }
159
+ await queryRunner.manager.save(order);
160
+ order.total_value = totalValue;
161
+ await queryRunner.manager.save(orderItems);
162
+
163
+ await queryRunner.commitTransaction();
164
+ await queryRunner.release();
165
+
166
+ return { ...order, order_items: orderItems };
167
+ } catch (err) {
168
+ await queryRunner.rollbackTransaction();
169
+ await queryRunner.release();
170
+ throw err;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Lấy order kèm với list item
176
+ */
177
+ async findOrderOrError(id: number) {
178
+ const order = await OrderEntity.findOne({
179
+ where: { id },
180
+ relations: [
181
+ 'order_items',
182
+ 'order_items.branch_menu',
183
+ 'order_items.branch_menu.menu_item',
184
+ ],
185
+ });
186
+ if (!order) {
187
+ throw new NotFoundException('Order id not found');
188
  }
189
+ return order;
190
+ }
191
+
192
+ async updateOrder(id: number, updateOrderDto: UpdateOrderDto) {
193
+ // update vì
194
+ // Offline
195
+ // Người dùng chuyển bàn
196
+ // Người dùng thêm/xoá món
197
+ // Cập nhật trạng thái
198
+ // Cập nhật rating
199
+ // Cập nhật note
200
+ }
201
+
202
+ async updateOrderPayment(id: number, paymentId: number) {
203
+ const order = await this.findOrderOrError(id);
204
+ order.payment_id = paymentId; // i'm trust that paymentId existed.
205
+ order.order_status = OrderStatus.ONLINE_PAID;
206
  await order.save();
 
 
 
207
  }
208
 
209
+ getOrderPaginteConfig() {
210
+ const paginateConfig: PaginateConfig<OrderEntity> = {
211
+ sortableColumns: ['id', 'created_at', 'total_value'],
212
+ filterableColumns: {
213
+ order_status: [FilterOperator.EQ],
214
+ create_at: [FilterOperator.GTE, FilterOperator.LTE],
215
+ updated_at: [FilterOperator.GTE, FilterOperator.LTE],
216
+ customer_id: [FilterOperator.EQ],
217
+ branch_id: [FilterOperator.EQ],
218
+ staff_id: [FilterOperator.EQ],
219
+ table_number: [FilterOperator.EQ],
220
+ rating: [FilterOperator.EQ, FilterOperator.GTE, FilterOperator.LTE],
221
+ total_value: [
222
+ FilterOperator.EQ,
223
+ FilterOperator.GTE,
224
+ FilterOperator.LTE,
225
+ ],
226
+ },
227
+ defaultSortBy: [['id', 'DESC']],
228
+ };
229
+ return paginateConfig;
230
+ }
231
+
232
+ getOrderQueryBuilder(branchId?: string) {
233
+ //
234
+ let queryBuilder = OrderEntity.createQueryBuilder('od')
235
+ .leftJoinAndSelect('od.order_items', 'order_items')
236
+ .leftJoinAndSelect('order_items.branch_menu', 'branch_menu')
237
+ .leftJoinAndSelect('branch_menu.menu_item', 'menu_item');
238
+ if (branchId) {
239
+ queryBuilder.andWhere('od.branch_id = :branchId', { branchId });
240
+ }
241
+ // console.log(queryBuilder.getQuery());
242
+ return queryBuilder;
243
+ }
244
+
245
+ async findAll(query: PaginateQuery, branchId?: string) {
246
+ const paginateConfig = this.getOrderPaginteConfig();
247
+ let queryBuilder = this.getOrderQueryBuilder(branchId);
248
+ if (branchId) {
249
+ queryBuilder.andWhere('od.branch_id = :branchId', { branchId });
250
+ }
251
+ return paginate(query, queryBuilder, paginateConfig);
252
+ }
253
+
254
+ async findHistory(query: PaginateQuery, userId: string, branchId?: string) {
255
+ const paginateConfig = this.getOrderPaginteConfig();
256
+ let queryBuilder = this.getOrderQueryBuilder(branchId);
257
+ queryBuilder.andWhere('od.customer_id = :userId', { userId });
258
+ return paginate(query, queryBuilder, paginateConfig);
259
  }
260
 
261
+ async findOne(id: number) {
262
+ return this.findOrderOrError(id);
263
  }
264
 
265
+ async remove(id: number) {
266
+ const order = await this.findOrderOrError(id);
267
+ return await OrderEntity.softRemove(order);
268
  }
269
  }
backend/src/modules/user/dto/update-users.dto.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsEmail, IsMobilePhone, IsOptional, IsString } from "class-validator";
2
+ import { Role } from "../../../common/enums/role.enum.js";
3
+ export class UpdateUsersDto {
4
+
5
+ @IsString()
6
+ id: string;
7
+
8
+ @IsOptional()
9
+ avatar: string;
10
+
11
+ @IsString()
12
+ @IsOptional()
13
+ full_name?: string;
14
+
15
+ @IsMobilePhone('vi-VN')
16
+ @IsOptional()
17
+ phone_number: string
18
+
19
+ @IsString()
20
+ @IsOptional()
21
+ address: string;
22
+
23
+ @IsEmail()
24
+ @IsOptional()
25
+ email?: string;
26
+
27
+ @IsOptional()
28
+ role: Role;
29
+
30
+ @IsString()
31
+ @IsOptional()
32
+ hash_password?: string;
33
+
34
+ @IsOptional()
35
+ is_valid: boolean;
36
+ }
37
+
backend/src/modules/user/user.controller.ts CHANGED
@@ -1,23 +1,61 @@
1
- import { Body, Controller, Get, Param, Post, Request } from '@nestjs/common';
2
- import { UserService } from './user.service.js';
3
- import { UserEntity } from 'src/entities/user.entity.js';
4
- import { UpdateUserDto } from './dto/update-user-dto.js';
5
-
6
- @Controller('users')
7
- export class UsersController {
8
- constructor(private readonly usersService: UserService) {}
9
-
10
- @Get(':username')
11
- async getUser(
12
- @Param('username') username: string,
13
- ): Promise<UserEntity | undefined> {
14
- return this.usersService.findOne(username);
15
- }
16
-
17
- @Post('updateUser')
18
- async updateUser(@Request() req){
19
- const userId = req.user.sub;
20
- const updateUserDto = req.body;
21
- return this.usersService.updateUserById(userId, updateUserDto);
22
- }
23
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Body, Controller, Get, Post, Put, Query, Request } from '@nestjs/common';
2
+ import { UserService } from './user.service.js';
3
+ import { UserEntity } from 'src/entities/user.entity.js';
4
+ import { Roles } from '../authentication/authorization/roles.decorator.js';
5
+ import { Role } from '../../common/enums/role.enum.js';
6
+ import { Paginate, PaginateQuery } from 'nestjs-paginate';
7
+ import { UpdateUsersDto } from './dto/update-users.dto.js';
8
+
9
+ @Controller('users')
10
+ export class UsersController {
11
+ constructor(private readonly usersService: UserService) {}
12
+
13
+ @Get('id')
14
+ @Roles(Role.ADMIN, Role.AREA_MANAGER, Role.BRANCH_MANAGER)
15
+ async getUserById(
16
+ @Query('id') id: string
17
+ ): Promise<UserEntity | undefined> {
18
+ return this.usersService.findOneByField("id", id)
19
+ }
20
+
21
+ @Get('fullname')
22
+ @Roles(Role.ADMIN, Role.AREA_MANAGER, Role.BRANCH_MANAGER)
23
+ async getUserByFullname( @Query('full_name') fullName: string, @Paginate() query: PaginateQuery, ) {
24
+ console.log(fullName)
25
+ return this.usersService.findAllByName(fullName, query);
26
+ }
27
+
28
+ @Post('updateUser')
29
+ async updateUser(@Request() req){
30
+ const userId = req.user.sub;
31
+ const updateUserDto = req.body;
32
+ return this.usersService.updateUserById(userId, updateUserDto);
33
+ }
34
+
35
+ @Put('updateList')
36
+ @Roles(Role.ADMIN)
37
+ async updateUsers(
38
+ @Body() updateUsersDto: UpdateUsersDto[],
39
+ ) {
40
+ return this.usersService.updateUsers(updateUsersDto);
41
+ }
42
+
43
+ @Get('getAll')
44
+ @Roles(Role.ADMIN, Role.AREA_MANAGER, Role.BRANCH_MANAGER)
45
+ async findAllUser(@Paginate() query: PaginateQuery) {
46
+ return this.usersService.findAllUser(query);
47
+ }
48
+
49
+ @Get('role')
50
+ @Roles(Role.ADMIN, Role.AREA_MANAGER, Role.BRANCH_MANAGER)
51
+ async getUserByRole( @Query('role') role: string, @Paginate() query: PaginateQuery, ) {
52
+ console.log(role)
53
+ return this.usersService.findAllByRole(role, query);
54
+ }
55
+
56
+ @Get('role')
57
+ @Roles(Role.ADMIN, Role.AREA_MANAGER)
58
+ async getUsersByBranch(@Query('branchId') branchId: string, @Paginate() query: PaginateQuery){
59
+
60
+ }
61
+ }
backend/src/modules/user/user.service.ts CHANGED
@@ -1,66 +1,167 @@
1
- import { Body, forwardRef, Inject, Injectable, NotFoundException } from '@nestjs/common';
2
- import { UserEntity } from '../../entities/user.entity.js';
3
- import { SignUpDto } from '../authentication/dto/sign-up.dto.js';
4
- import { UpdateUserDto } from './dto/update-user-dto.js';
5
- import { ValidateService } from '../../validate/validate.service.js';
6
- import * as bcrypt from 'bcrypt';
7
- import { JwtService } from '@nestjs/jwt';
8
-
9
- export type User = any;
10
-
11
- @Injectable()
12
- export class UserService {
13
- constructor(
14
- private validateService: ValidateService,
15
- private jwtService: JwtService,
16
- ) {}
17
-
18
- async findOne(username: string): Promise<UserEntity | undefined> {
19
- return UserEntity.findOne({ where: { full_name: username } });
20
- }
21
-
22
- async create(signUpDto: SignUpDto): Promise<UserEntity | undefined>{
23
- return UserEntity.create({
24
- full_name: signUpDto.full_name,
25
- phone_number: signUpDto.phone_number,
26
- email: signUpDto.email,
27
- hash_password: signUpDto.password
28
- })
29
- }
30
-
31
- async save(userEntity: UserEntity): Promise<UserEntity | undefined>{
32
- return UserEntity.save(userEntity);
33
- }
34
-
35
- async findOneByField(field: string, value: any): Promise<UserEntity | undefined> {
36
- return UserEntity.findOne({
37
- where: { [field]: value }
38
- });
39
- }
40
-
41
- async updateUserById(userId: string, updateUserDto: UpdateUserDto){
42
-
43
- await this.validateService.checkExistField('email', updateUserDto.email);
44
- await this.validateService.checkExistField('phone_number', updateUserDto.phone_number);
45
-
46
- const user = await UserEntity.findOne({
47
- where: { id: userId }
48
- });
49
- if (!user) {
50
- throw new NotFoundException(`User with ID ${userId} not found`);
51
- }
52
-
53
- Object.assign(user, updateUserDto);
54
- if (updateUserDto.hash_password) {
55
- const saltRounds = 10;
56
- user.hash_password = await bcrypt.hash(updateUserDto.hash_password, saltRounds); // Mã hóa mật khẩu
57
- }
58
- await UserEntity.save(user);
59
-
60
- const payload = { sub: user.id, username: user.full_name, roles: user.role };
61
- const token = await this.jwtService.signAsync(payload)
62
- return {
63
- access_token: token
64
- };
65
- }
66
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Body, forwardRef, HttpStatus, Inject, Injectable, NotFoundException } from '@nestjs/common';
2
+ import { UserEntity } from '../../entities/user.entity.js';
3
+ import { SignUpDto } from '../authentication/dto/sign-up.dto.js';
4
+ import { UpdateUserDto } from './dto/update-user-dto.js';
5
+ import { ValidateService } from '../../validate/validate.service.js';
6
+ import * as bcrypt from 'bcrypt';
7
+ import { JwtService } from '@nestjs/jwt';
8
+ import { FilterOperator, paginate, PaginateConfig, PaginateQuery } from 'nestjs-paginate';
9
+ import { UpdateUsersDto } from './dto/update-users.dto.js';
10
+ import { In } from 'typeorm';
11
+
12
+ export type User = any;
13
+
14
+ @Injectable()
15
+ export class UserService {
16
+ constructor(
17
+ private validateService: ValidateService,
18
+ private jwtService: JwtService,
19
+ ) { }
20
+
21
+ async create(signUpDto: SignUpDto): Promise<UserEntity | undefined> {
22
+ return UserEntity.create({
23
+ full_name: signUpDto.full_name,
24
+ phone_number: signUpDto.phone_number,
25
+ email: signUpDto.email,
26
+ hash_password: signUpDto.password
27
+ })
28
+ }
29
+
30
+ async save(userEntity: UserEntity): Promise<UserEntity | undefined> {
31
+ return UserEntity.save(userEntity);
32
+ }
33
+
34
+ async findOneByField(field: string, value: any): Promise<UserEntity | undefined> {
35
+ return UserEntity.findOne({
36
+ where: { [field]: value }
37
+ });
38
+ }
39
+
40
+ async updateUserById(userId: string, updateUserDto: UpdateUserDto) {
41
+
42
+ await this.validateService.checkExistField('email', updateUserDto.email);
43
+ await this.validateService.checkExistField('phone_number', updateUserDto.phone_number);
44
+
45
+ const user = await UserEntity.findOne({
46
+ where: { id: userId }
47
+ });
48
+ if (!user) {
49
+ throw new NotFoundException(`User with ID ${userId} not found`);
50
+ }
51
+
52
+ Object.assign(user, updateUserDto);
53
+ if (updateUserDto.hash_password) {
54
+ const saltRounds = 10;
55
+ user.hash_password = await bcrypt.hash(updateUserDto.hash_password, saltRounds); // Mã hóa mật khẩu
56
+ }
57
+ await UserEntity.save(user);
58
+
59
+ const payload = { sub: user.id, username: user.full_name, roles: user.role };
60
+ const token = await this.jwtService.signAsync(payload)
61
+ return {
62
+ access_token: token
63
+ };
64
+ }
65
+
66
+ async findAllUser(query: PaginateQuery) {
67
+ const paginateConfig: PaginateConfig<UserEntity> = {
68
+ sortableColumns: ['id', 'full_name', 'phone_number', 'email'],
69
+ nullSort: 'last',
70
+ defaultSortBy: [['id', 'DESC']],
71
+ searchableColumns: ['full_name'],
72
+ filterableColumns: {
73
+ full_name: [
74
+ FilterOperator.LT,
75
+ FilterOperator.LTE,
76
+ FilterOperator.GT,
77
+ FilterOperator.GTE,
78
+ ],
79
+ item_type: [FilterOperator.EQ]
80
+ },
81
+ };
82
+ return paginate(query, UserEntity.createQueryBuilder(), paginateConfig);
83
+ }
84
+
85
+ async findAllByName(fullName: string, query: PaginateQuery) {
86
+ const queryBuilder = UserEntity.createQueryBuilder('users')
87
+ .where('users.full_name = :fullName', { fullName });
88
+ const paginateConfig: PaginateConfig<UserEntity> = {
89
+ sortableColumns: ['id', 'full_name', 'phone_number', 'email'],
90
+ nullSort: 'last',
91
+ defaultSortBy: [['id', 'DESC']],
92
+ searchableColumns: ['full_name'],
93
+ filterableColumns: {
94
+ full_name: [
95
+ FilterOperator.LT,
96
+ FilterOperator.LTE,
97
+ FilterOperator.GT,
98
+ FilterOperator.GTE,
99
+ ],
100
+ item_type: [FilterOperator.EQ]
101
+ },
102
+ };
103
+ return paginate(query, queryBuilder, paginateConfig);
104
+ }
105
+
106
+ async findAllByRole(role: string, query: PaginateQuery) {
107
+ const queryBuilder = UserEntity.createQueryBuilder('users')
108
+ .where('users.role = :role', { role });
109
+ const paginateConfig: PaginateConfig<UserEntity> = {
110
+ sortableColumns: ['id', 'full_name', 'phone_number', 'email'],
111
+ nullSort: 'last',
112
+ defaultSortBy: [['id', 'DESC']],
113
+ searchableColumns: ['full_name'],
114
+ filterableColumns: {
115
+ full_name: [
116
+ FilterOperator.LT,
117
+ FilterOperator.LTE,
118
+ FilterOperator.GT,
119
+ FilterOperator.GTE,
120
+ ],
121
+ item_type: [FilterOperator.EQ]
122
+ },
123
+ };
124
+ return paginate(query, queryBuilder, paginateConfig);
125
+ }
126
+
127
+ async updateUsers(updateUsersDto: UpdateUsersDto[]) {
128
+ try {
129
+ //Lấy ra id trong updateUsersDto
130
+ const userIds = updateUsersDto.map(user => user.id).filter(id => id !== undefined);
131
+
132
+ //Lấy ra các user tồn tại trong db
133
+ const existingUsers = await UserEntity.find({
134
+ where: { id: In(userIds) },
135
+ });
136
+
137
+ // Bước 2: Kết hợp dữ liệu từ yêu cầu với dữ liệu hiện có
138
+ const mergedData: Partial<User>[] = [];
139
+ for (const userData of updateUsersDto) {
140
+ if (userData.hash_password)
141
+ userData.hash_password = await bcrypt.hash(userData.hash_password, 10);
142
+
143
+ const existingUser = existingUsers.find(user => user.id === userData.id);
144
+
145
+ if (existingUser) {
146
+ // Merge data: keep existing fields for any data not included in the request
147
+ mergedData.push({ ...existingUser, ...userData });
148
+ } else {
149
+ // For new users, use only the request data
150
+ mergedData.push(userData);
151
+ }
152
+ }
153
+
154
+ console.log(updateUsersDto)
155
+ UserEntity.upsert(mergedData, ['id'])
156
+ return {
157
+ statusCode: HttpStatus.OK,
158
+ message: "Thành công"
159
+ }
160
+ } catch (error) {
161
+ return {
162
+ statusCode: HttpStatus.OK,
163
+ message: "Thất bại"
164
+ }
165
+ }
166
+ }
167
+ }
backend/src/payment/dto/create-payment-url.dto.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsNumber, IsOptional, IsString } from "class-validator"
2
+
3
+ export class CreatePaymentUrlDto {
4
+ @IsNumber()
5
+ amount: number
6
+
7
+ @IsOptional()
8
+ @IsString()
9
+ bankCode: string
10
+
11
+ @IsString()
12
+ orderId: string
13
+
14
+ @IsString()
15
+ orderDescription: string
16
+
17
+ @IsString()
18
+ orderType: string
19
+
20
+ @IsString()
21
+ language: string
22
+ }
backend/src/payment/dto/create-payment.dto..ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsNumber, IsOptional, IsString } from 'class-validator';
2
+
3
+ export class CreatePaymentDto {
4
+ @IsNumber()
5
+ payment_method: number;
6
+
7
+ @IsNumber()
8
+ vnp_amount: number
9
+
10
+ @IsString()
11
+ vnp_bank_code: string
12
+
13
+ @IsString()
14
+ vnp_bank_tran_no: string
15
+
16
+ @IsNumber()
17
+ vnp_card_type: number
18
+
19
+ @IsString()
20
+ @IsOptional()
21
+ vnp_order_info: string //Nội dung giao dịch
22
+
23
+ @IsString()
24
+ @IsOptional()
25
+ vnp_paydate: string
26
+
27
+ @IsString()
28
+ @IsOptional()
29
+ vnp_response_code: number
30
+
31
+ @IsString()
32
+ @IsOptional()
33
+ vnp_transaction_no: string
34
+
35
+ @IsString()
36
+ @IsOptional()
37
+ vnp_transaction_status: number
38
+ }
backend/src/payment/payment.controller.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // payment.controller.ts
2
+ import { Controller, Post, Body, Req, Res, Get } from '@nestjs/common';
3
+ import { PaymentService } from './payment.service.js';
4
+ import { Request, Response } from 'express';
5
+ import { Public } from '../modules/authentication/authentication.decorator.js';
6
+ import { CreatePaymentUrlDto } from './dto/create-payment-url.dto.js';
7
+
8
+ @Controller('payment')
9
+ export class PaymentController {
10
+ constructor(private readonly paymentService: PaymentService) {}
11
+
12
+ @Public()
13
+ @Post('create_payment_url')
14
+ async createPaymentUrl(@Req() req: Request, @Body() body: CreatePaymentUrlDto) {
15
+
16
+ const ipAddr =
17
+ req.headers['x-forwarded-for'] ||
18
+ req.socket.remoteAddress ||
19
+ req.socket?.remoteAddress;
20
+
21
+ return await this.paymentService.createPaymentUrl(
22
+ body.amount,
23
+ body.orderId,
24
+ body.orderDescription,
25
+ body.orderType,
26
+ body.language,
27
+ ipAddr as string,
28
+ );
29
+ }
30
+
31
+ @Public()
32
+ @Get('vnpay_ipn')
33
+ async vnpayIpn(@Req() req: Request, @Body() body: any){
34
+ const reqQuery = req.query;
35
+ const res = await this.paymentService.vnpayIpn(reqQuery)
36
+ console.log(res);
37
+ return res;
38
+ }
39
+ }
backend/src/payment/payment.module.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from '@nestjs/common';
2
+ import { PaymentService } from './payment.service.js';
3
+ import { PaymentController } from './payment.controller.js';
4
+ import { OrderService } from '../modules/order/order.service.js';
5
+ import { BranchService } from '../modules/branch/branch.service.js';
6
+
7
+ @Module({
8
+ controllers: [PaymentController],
9
+ providers: [PaymentService, OrderService, BranchService],
10
+ })
11
+ export class PaymentModule {}
backend/src/payment/payment.service.ts ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // payment.service.ts
2
+ import { HttpStatus, Injectable } from '@nestjs/common';
3
+ import { ConfigService } from '@nestjs/config';
4
+ import * as querystring from 'qs';
5
+ import * as crypto from 'crypto';
6
+ import { OrderService } from '../modules/order/order.service.js';
7
+ import { PaymentEntity } from '../entities/payment.entity.js';
8
+ import { CreatePaymentUrlDto } from './dto/create-payment-url.dto.js';
9
+ import { CreatePaymentDto } from './dto/create-payment.dto..js';
10
+ import { VnpCardType } from '../common/enums/VnpCardType.enum.js';
11
+
12
+ @Injectable()
13
+ export class PaymentService {
14
+ constructor(
15
+ private readonly configService: ConfigService,
16
+ private readonly orderService: OrderService
17
+ ) { }
18
+
19
+ async createPaymentUrl(amount: number, orderId: string, orderDescription: string, orderType: string, language: string, ipAddr: string) {
20
+
21
+ const tmnCode = this.configService.get<string>('vnp_TmnCode');
22
+ const secretKey = this.configService.get<string>('vnp_HashSecret');
23
+ const vnpUrl = this.configService.get<string>('vnp_Url');
24
+ const returnUrl = this.configService.get<string>('vnp_ReturnUrl');
25
+
26
+ const date = new Date();
27
+ const createDate = this.formatDate(date, 'yyyymmddHHmmss');
28
+ const locale = language || 'vn';
29
+ const currCode = 'VND';
30
+
31
+ const vnp_Params: Record<string, string> = {
32
+ vnp_Version: '2.1.0',
33
+ vnp_Command: 'pay',
34
+ vnp_TmnCode: tmnCode,
35
+ vnp_Locale: locale,
36
+ vnp_CurrCode: currCode,
37
+ vnp_TxnRef: orderId,
38
+ vnp_OrderInfo: orderDescription,
39
+ vnp_OrderType: orderType,
40
+ vnp_Amount: (amount * 100).toString(),
41
+ vnp_ReturnUrl: returnUrl,
42
+ vnp_IpAddr: ipAddr,
43
+ vnp_CreateDate: createDate,
44
+ };
45
+ console.log("3")
46
+
47
+ const sortedParams = this.sortObject(vnp_Params);
48
+
49
+ // Sign the data
50
+ const signData = querystring.stringify(sortedParams, { encode: false });
51
+ const hmac = crypto.createHmac('sha512', secretKey);
52
+ const signed = hmac.update(Buffer.from(signData, 'utf-8')).digest('hex');
53
+ sortedParams['vnp_SecureHash'] = signed;
54
+
55
+ // Create the URL
56
+ const res = `${vnpUrl}?${querystring.stringify(sortedParams, { encode: false })}`;
57
+
58
+ return res;
59
+ }
60
+
61
+
62
+ async vnpayIpn(reqQuery) {
63
+ console.log("helloooo")
64
+ let vnp_Params = reqQuery;
65
+ let secureHash = vnp_Params['vnp_SecureHash'];
66
+
67
+ let orderId = vnp_Params['vnp_TxnRef'];
68
+ let rspCode = vnp_Params['vnp_ResponseCode'];
69
+
70
+ delete vnp_Params['vnp_SecureHash'];
71
+ delete vnp_Params['vnp_SecureHashType'];
72
+
73
+ vnp_Params = this.sortObject(vnp_Params);
74
+ let secretKey = this.configService.get('vnp_HashSecret');
75
+ let signData = querystring.stringify(vnp_Params, { encode: false });
76
+ let hmac = crypto.createHmac("sha512", secretKey);
77
+ let signed = hmac.update(Buffer.from(signData, 'utf-8')).digest("hex");
78
+
79
+ let paymentStatus = '0'; // Giả sử '0' là trạng thái khởi tạo giao dịch, chưa có IPN. Trạng thái này được lưu khi yêu cầu thanh toán chuyển hướng sang Cổng thanh toán VNPAY tại đầu khởi tạo đơn hàng.
80
+ //let paymentStatus = '1'; // Giả sử '1' là trạng thái thành công bạn cập nhật sau IPN được gọi và trả kết quả về nó
81
+ //let paymentStatus = '2'; // Giả sử '2' là trạng thái thất bại bạn cập nhật sau IPN được gọi và trả kết quả về nó
82
+ //Kiểm tra có đúng order id không
83
+ let checkOrderId = true;
84
+ let order;
85
+ try {
86
+ order = await this.orderService.findOne(orderId)
87
+ } catch (error) {
88
+ return {
89
+ statusCode: HttpStatus.OK,
90
+ message: 'Order not found',
91
+ };
92
+ }
93
+ console.log("order = ", order);
94
+ console.log("order total value ", order.total_value);
95
+ // Kiểm tra số tiền "giá trị của vnp_Amout/100" trùng khớp với số tiền của đơn hàng trong CSDL của bạn
96
+ let checkAmount = order.total_value == parseFloat(vnp_Params["vnp_Amount"])/100;
97
+ if (secureHash === signed) { //kiểm tra checksum
98
+ if (checkOrderId) {
99
+ if (checkAmount) {
100
+ if (paymentStatus == "0") { //kiểm tra tình trạng giao dịch trước khi cập nhật tình trạng thanh toán
101
+ if (rspCode == "00") {
102
+ //thanh cong
103
+ //paymentStatus = '1'
104
+ // Ở đây cập nhật trạng thái giao dịch thanh toán thành công vào CSDL của bạn
105
+ const payment = await this.create({
106
+ payment_method: 2,
107
+ vnp_amount: parseFloat(vnp_Params["vnp_Amount"])/100,
108
+ vnp_bank_code: vnp_Params['vnp_BankCode'],
109
+ vnp_bank_tran_no: vnp_Params['vnp_BankTranNo'],
110
+ vnp_card_type: VnpCardType[vnp_Params['vnp_CardType'] as keyof typeof VnpCardType],
111
+ vnp_order_info: vnp_Params['vnp_BankTranNo'],
112
+ vnp_paydate: vnp_Params['vnp_PayDate'],
113
+ vnp_response_code: vnp_Params['vnp_ResponseCode'],
114
+ vnp_transaction_no: vnp_Params['vnp_TransactionNo'],
115
+ vnp_transaction_status: vnp_Params['vnp_TransactionStatus'],
116
+ });
117
+ await PaymentEntity.save(payment);
118
+ this.orderService.updateOrderPayment(orderId, payment.id)
119
+ return {
120
+ statusCode: HttpStatus.OK,
121
+ message: 'Thành công!',
122
+ };
123
+ }
124
+ else {
125
+ //that bai
126
+ //paymentStatus = '2'
127
+ // Ở đây cập nhật trạng thái giao dịch thanh toán thất bại vào CSDL của bạn
128
+ return {
129
+ statusCode: HttpStatus.OK,
130
+ message: 'Thất bại',
131
+ };
132
+ }
133
+ }
134
+ else {
135
+ return {
136
+ statusCode: HttpStatus.OK,
137
+ message: 'This order has been updated to the payment status',
138
+ };
139
+ }
140
+ }
141
+ else {
142
+ return {
143
+ statusCode: HttpStatus.OK,
144
+ message: 'Amount invalid',
145
+ };
146
+ }
147
+ }
148
+ else {
149
+ return {
150
+ statusCode: HttpStatus.OK,
151
+ message: 'Order not found',
152
+ };
153
+ }
154
+ }
155
+ else {
156
+ return {
157
+ statusCode: HttpStatus.OK,
158
+ message: 'Checksum failed!',
159
+ };
160
+ }
161
+ }
162
+
163
+ // Format date helper function
164
+ formatDate(date: Date, format: string): string {
165
+ const yyyymmdd = date.toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
166
+ const hhmmss = date.toTimeString().slice(0, 8).replace(/:/g, ''); // HHMMSS
167
+ return format === 'yyyymmddHHmmss' ? yyyymmdd + hhmmss : hhmmss;
168
+ }
169
+
170
+ sortObject(obj) {
171
+ let sorted = {};
172
+ let str = [];
173
+ let key;
174
+ for (key in obj) {
175
+ if (obj.hasOwnProperty(key)) {
176
+ str.push(encodeURIComponent(key));
177
+ }
178
+ }
179
+ str.sort();
180
+ for (key = 0; key < str.length; key++) {
181
+ sorted[str[key]] = encodeURIComponent(obj[str[key]]).replace(/%20/g, "+");
182
+ }
183
+ return sorted;
184
+ }
185
+
186
+ async create(createPaymentDto: CreatePaymentDto): Promise<PaymentEntity | undefined> {
187
+ return PaymentEntity.create({
188
+ payment_method: 2,
189
+ vnp_amount: createPaymentDto.vnp_amount,
190
+ vnp_bank_code: createPaymentDto.vnp_bank_code,
191
+ vnp_bank_tran_no: createPaymentDto.vnp_bank_tran_no,
192
+ vnp_card_type: createPaymentDto.vnp_card_type,
193
+ vnp_order_info: createPaymentDto.vnp_order_info,
194
+ vnp_paydate: createPaymentDto.vnp_paydate,
195
+ vnp_response_code: createPaymentDto.vnp_response_code,
196
+ vnp_transaction_no: createPaymentDto.vnp_transaction_no,
197
+ vnp_transaction_status: createPaymentDto.vnp_transaction_status
198
+ })
199
+ }
200
+ }
frontend/.gitignore CHANGED
@@ -24,4 +24,7 @@ yarn-error.log*
24
 
25
  .env
26
  note_dev.txt
27
- object.json
 
 
 
 
24
 
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",