diff --git a/Dockerfile b/Dockerfile index d150cb82d8dd48cf635d48fb583ac812a90524c5..e9d2e96bb2c82a48fdafd9f0d9768084818aefaa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Use an official Node.js runtime as a parent image -FROM node:22-alpine +FROM node:22.4.0 # Set the working directory WORKDIR /app diff --git a/README.md b/README.md index e36add33676598e65d0691c15601ecc3c0f49d26..86fd6e1a4ebfa4cb83bd3773ccc3e8344a492adf 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,4 @@ app_port: 3000 - [Trello](https://trello.com/w/pbl6hthngthongtin1) - [Google drive](https://drive.google.com/drive/folders/19qQ5Wn4uat0hSeDX3N5c0X19abYkQLfc?usp=drive_link) +- [API docs](https://hackmd.io/NCILuSy3Rxif-KRVQZjNIg#Menu-item) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 6636f9f180e9a1849878b2c5c93229b1e4dead06..c2cb838bc0968216339b9de28bf857bdd9a75df9 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -12,6 +12,8 @@ import { BranchModule } from './modules/branch/branch.module.js'; import { AuthenticationModule } from './modules/authentication/authentication.module.js'; import { MenuItemModule } from './modules/menu-item/menu-item.module.js'; import { FeedsModule } from './modules/feeds/feeds.module.js'; +import { OrderModule } from './modules/order/order.module.js'; +import { BranchMenusModule } from './modules/branch-menus/branch-menus.module.js'; import { PaymentModule } from './payment/payment.module.js'; @Module({ imports: [ @@ -28,7 +30,9 @@ import { PaymentModule } from './payment/payment.module.js'; AuthenticationModule, MenuItemModule, FeedsModule, - PaymentModule, + OrderModule, + BranchMenusModule, + PaymentModule, ], controllers: [AppController], providers: [AppService], diff --git a/backend/src/common/enums/MenuItemType.enum.ts b/backend/src/common/enums/MenuItemType.enum.ts index 325fcb8284d1d6ea7d17b6bbc8945d5239a2dde8..4a56d13c2b7e32f0e280739fad8d287ef1afdc68 100644 --- a/backend/src/common/enums/MenuItemType.enum.ts +++ b/backend/src/common/enums/MenuItemType.enum.ts @@ -1,6 +1,6 @@ export enum MenuItemType { - MON_CHINH = 'monchinh', // món chính - TRANG_MIENG = 'trangmieng', // tráng miệng - GIAI_KHAT = 'giaikhat', // giải khát - KHAC = 'khac', // khác + MON_CHINH = 1, // món chính + TRANG_MIENG = 2, // tráng miệng + GIAI_KHAT = 3, // giải khát + KHAC = 0, // khác } diff --git a/backend/src/common/enums/OrderStatus.enum.ts b/backend/src/common/enums/OrderStatus.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..177e6b482e755c21ba77496bf041ff97d7204e7b --- /dev/null +++ b/backend/src/common/enums/OrderStatus.enum.ts @@ -0,0 +1,7 @@ +export enum OrderStatus { + PENDING = 0, // KH đặt hàng đã thanh toán online, nhân viên chưa xác nhận + CONFIRMED = 1, // nhân viên xác nhận và chuyển sang trạng thái + PREPARING = 2, // nhân viên xác nhận và sang trạng thái preparing này ngay lập tức + DELIVERING = 3, // dang giao hàng + DONE = 4, // +} diff --git a/backend/src/common/enums/OrderType.enum.ts b/backend/src/common/enums/OrderType.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..17a553d203d7ce2b2d73529ba8d8bf514ffbf886 --- /dev/null +++ b/backend/src/common/enums/OrderType.enum.ts @@ -0,0 +1,5 @@ +export enum OrderType { + TAKE_AWAY = 0, + OFFLINE = 1, + ONLINE = 2, +} diff --git a/backend/src/common/enums/PaymentMethod.enum.ts b/backend/src/common/enums/PaymentMethod.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..96d2a5d0e6ad4ab22ef30f3421575be68dcba0e2 --- /dev/null +++ b/backend/src/common/enums/PaymentMethod.enum.ts @@ -0,0 +1,5 @@ +export enum PaymentMethod { + CASH = 0, + CARD = 1, + ONLINE_PAYMENT = 2, +} diff --git a/backend/src/common/enums/role.enum.ts b/backend/src/common/enums/role.enum.ts index 983c8e3dcd0f8ba750a6708e4abf562b3a34cfd7..803de759b14bdd27f5c507b552dcb471f31ec50d 100644 --- a/backend/src/common/enums/role.enum.ts +++ b/backend/src/common/enums/role.enum.ts @@ -1,8 +1,8 @@ -export enum Role { - CUSTOMER = 'CUSTOMER', - ADMIN = 'ADMIN', - BRANCH_MANAGER = 'BRANCH_MANAGER', - AREA_MANAGER = 'AREA_MANAGER', - STAFF = 'STAFF', - SHIPPER = 'SHIPPER' - } \ No newline at end of file +export enum Role { + CUSTOMER = 'CUSTOMER', + ADMIN = 'ADMIN', + BRANCH_MANAGER = 'BRANCH_MANAGER', + AREA_MANAGER = 'AREA_MANAGER', + STAFF = 'STAFF', + SHIPPER = 'SHIPPER', +} diff --git a/backend/src/entities/branch-menu.entity.ts b/backend/src/entities/branch-menu.entity.ts index ccbd06aee52849fa24438ab0704e7ec9607211b4..39e94701a0defc9b1ca4ab6ccef37aa83c706c49 100644 --- a/backend/src/entities/branch-menu.entity.ts +++ b/backend/src/entities/branch-menu.entity.ts @@ -2,6 +2,7 @@ import { BaseEntity, Column, Entity, + JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn, @@ -21,15 +22,20 @@ export class BranchMenuEntity extends BaseEntity { @Column() menu_id: string; - @Column() + @Column({ nullable: true }) description: string; - @Column() + @Column({ default: true }) is_open: boolean; + @Column({ default: 0 }) + sold_count: number; + @ManyToOne(() => BranchEntity, (a) => a.menu_items) + @JoinColumn({ name: 'branch_id' }) branch: Relation; @ManyToOne(() => MenuItemEntity, (a) => a.branch_menus) + @JoinColumn({ name: 'menu_id' }) menu_item: Relation; } diff --git a/backend/src/entities/branch.entity.ts b/backend/src/entities/branch.entity.ts index 175d989664d3a6b9208d840d8c8411bd548ff14f..f0f3d8e7e3e1eecabe11ad652d6b67b8fc8a8fb2 100644 --- a/backend/src/entities/branch.entity.ts +++ b/backend/src/entities/branch.entity.ts @@ -1,35 +1,44 @@ -import { - BaseEntity, - Column, - Entity, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, - Relation, -} from 'typeorm'; -import { UserEntity } from './user.entity.js'; -import { BranchMenuEntity } from './branch-menu.entity.js'; - -@Entity('branches') -export class BranchEntity extends BaseEntity { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column() - name: string; - - @Column() - location: string; - - @Column() - phone_number: string; - - @ManyToOne(() => UserEntity, (user) => user.branches) - owner: Relation; - - @OneToMany(() => BranchMenuEntity, (a) => a.branch) - menu_items: Relation[]; - - @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) - create_at: Date; -} +import { + BaseEntity, + Column, + CreateDateColumn, + Entity, + ManyToOne, + OneToMany, + PrimaryColumn, + PrimaryGeneratedColumn, + Relation, +} from 'typeorm'; +import { UserEntity } from './user.entity.js'; +import { BranchMenuEntity } from './branch-menu.entity.js'; +import { ReceiptEntity } from './receipt.entity.js'; + +@Entity('branches') +export class BranchEntity extends BaseEntity { + @PrimaryColumn() + id: string; + + @Column({ nullable: true }) + name: string; + + @Column({ nullable: true }) + image_url: string; + + @Column({ nullable: true }) + location: string; + + @Column({ nullable: true }) + phone_number: string; + + @ManyToOne(() => UserEntity, (user) => user.branches) + owner: Relation; + + @OneToMany(() => BranchMenuEntity, (a) => a.branch) + menu_items: Relation[]; + + @OneToMany(() => ReceiptEntity, (a) => a.branch) + receipts: Relation[]; + + @CreateDateColumn() + create_at: Date; +} diff --git a/backend/src/entities/feed.entity.ts b/backend/src/entities/feed.entity.ts index f85f15adc24fffba83a0148c0fa0e61589f05989..170c9cf1f63a01722e4f1abc634c5170cc2b180e 100644 --- a/backend/src/entities/feed.entity.ts +++ b/backend/src/entities/feed.entity.ts @@ -1,4 +1,4 @@ -import { Entity, Column, BaseEntity, PrimaryGeneratedColumn } from 'typeorm'; +import { Entity, Column, BaseEntity, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm'; @Entity('feeds') export class FeedEntity extends BaseEntity { @@ -17,6 +17,6 @@ export class FeedEntity extends BaseEntity { @Column({ nullable: true }) description: string; - @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + @CreateDateColumn() create_at: Date; } diff --git a/backend/src/entities/menu-item.entity.ts b/backend/src/entities/menu-item.entity.ts index f5c36770bea96f4ab53ae2b4f9acc02dc25f2f37..a47cb8ef34200331a36f61be7d1d6ce155836ed4 100644 --- a/backend/src/entities/menu-item.entity.ts +++ b/backend/src/entities/menu-item.entity.ts @@ -1,6 +1,7 @@ import { BaseEntity, Column, + CreateDateColumn, Entity, OneToMany, PrimaryColumn, @@ -20,8 +21,8 @@ export class MenuItemEntity extends BaseEntity { @Column({ nullable: true }) image_url: string; - @Column({ type: 'enum', enum: MenuItemType, default: 'khac' }) - item_type: MenuItemType; + @Column({ default: 0 }) + item_type: number; @Column() description: string; @@ -32,6 +33,6 @@ export class MenuItemEntity extends BaseEntity { @OneToMany(() => BranchMenuEntity, (a) => a.menu_item) branch_menus: Relation[]; - @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + @CreateDateColumn() create_at: Date; } diff --git a/backend/src/entities/order-item.entity.ts b/backend/src/entities/order-item.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..17b402026294ba9cbf370c5ceb7fe3fc85abe8ba --- /dev/null +++ b/backend/src/entities/order-item.entity.ts @@ -0,0 +1,37 @@ +import { + BaseEntity, + Column, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Relation, +} from 'typeorm'; +import { BranchMenuEntity } from './branch-menu.entity.js'; +import { OrderEntity } from './order.entity.js'; + +@Entity('order_items') +export class OrderItemEntity extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column() + order_id: number; + + @ManyToOne(() => OrderEntity, (a) => a.order_items) + @JoinColumn({ name: 'order_id' }) + order: Relation; + + @Column() + branch_menu_id: string; + + @ManyToOne(() => BranchMenuEntity) + @JoinColumn({ name: 'branch_menu_id' }) + branch_menu: Relation; + + @Column() + quantity: number; + + @Column() + price: number; +} diff --git a/backend/src/entities/order.entity.ts b/backend/src/entities/order.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..70d8a26dc27a75a4888cc2a9217e8024fb2b0420 --- /dev/null +++ b/backend/src/entities/order.entity.ts @@ -0,0 +1,70 @@ +import { + BaseEntity, + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + OneToOne, + PrimaryGeneratedColumn, + Relation, +} from 'typeorm'; +import { BranchEntity } from './branch.entity.js'; +import { UserEntity } from './user.entity.js'; +import { OrderType } from '../common/enums/OrderType.enum.js'; +import { OrderStatus } from '../common/enums/OrderStatus.enum.js'; +import { OrderItemEntity } from './order-item.entity.js'; +import { PaymentEntity } from './payment.entity.js'; + +@Entity('orders') +export class OrderEntity extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: true }) + customer_id: string; + + @ManyToOne(() => UserEntity, { nullable: true }) + @JoinColumn({ name: 'customer_id' }) + customer: Relation; + + @Column() + branch_id: string; + + @ManyToOne(() => BranchEntity) + @JoinColumn({ name: 'branch_id' }) + branch: Relation; + + @Column({ nullable: true }) + staff_id: string; + + @ManyToOne(() => UserEntity, { nullable: true }) + @JoinColumn({ name: 'staff_id' }) + staff: Relation; + + @Column({ nullable: true }) + table_number: number; + + @Column() + total_value: number; + + @CreateDateColumn() + create_at: Date; + + @Column({ default: 0 }) + order_type: number; + + @Column({ default: 0 }) + order_status: number; + + @OneToMany(() => OrderItemEntity, (a) => a.order) + order_items: Relation[]; + + @Column({ nullable: true }) + payment_id: number; + + @OneToOne(() => PaymentEntity, (a) => a.order) + @JoinColumn({ name: 'payment_id' }) + payment: Relation; +} diff --git a/backend/src/entities/payment.entity.ts b/backend/src/entities/payment.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b1f23547d72594d035c201f10b4f42b578baaeb --- /dev/null +++ b/backend/src/entities/payment.entity.ts @@ -0,0 +1,25 @@ +import { + BaseEntity, + Column, + Entity, + OneToOne, + PrimaryGeneratedColumn, + Relation, +} from 'typeorm'; +import { OrderEntity } from './order.entity.js'; +import { PaymentMethod } from '../common/enums/PaymentMethod.enum.js'; + +@Entity('payments') +export class PaymentEntity extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @OneToOne(() => OrderEntity, (a) => a.payment) + order: Relation; + + @Column({ default: 0 }) + payment_method: number; // E.g., 'Cash', 'Credit Card', 'Online Payment' + + @Column() + value: number; +} diff --git a/backend/src/entities/receipt.entity.ts b/backend/src/entities/receipt.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..2881e8ca4d514c0f49f65e4e62c4a1326dee9220 --- /dev/null +++ b/backend/src/entities/receipt.entity.ts @@ -0,0 +1,57 @@ +import { + BaseEntity, + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Relation, +} from 'typeorm'; +import { BranchEntity } from './branch.entity.js'; +import { UserEntity } from './user.entity.js'; + +@Entity('receipts') +export class ReceiptEntity extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column() + branch_id: string; + + @ManyToOne(() => BranchEntity, (a) => a.receipts) + @JoinColumn({ name: 'branch_id' }) + branch: Relation; + + @Column({ default: 0 }) + income: number; + + @Column({ default: 0 }) + spend: number; + + @Column({ nullable: true }) + description: string; + + @Column({ default: 0 }) + type: number; + + @Column({ default: 0 }) + sub_type: number; + + @Column({ nullable: true }) + sender_id: string; + + @Column({ nullable: true }) + receiver_id: string; + + @ManyToOne(() => UserEntity, (a) => a.out_receipts) + @JoinColumn({ name: 'sender_id' }) + sender: Relation; + + @ManyToOne(() => UserEntity, (a) => a.in_receipts) + @JoinColumn({ name: 'receiver_id' }) + receiver: Relation; + + @CreateDateColumn() + created_at: Date; +} diff --git a/backend/src/entities/user.entity.ts b/backend/src/entities/user.entity.ts index 0492c9028a85fa33a07cf45d2b00632f51d572c6..1b2e80a3c5c431946319fdcbb6b2c39fe79bd256 100644 --- a/backend/src/entities/user.entity.ts +++ b/backend/src/entities/user.entity.ts @@ -1,54 +1,59 @@ -import { - Entity, - Column, - BaseEntity, - PrimaryGeneratedColumn, - OneToMany, - ManyToOne, - Relation, - JoinColumn, -} from 'typeorm'; -import { BranchEntity } from './branch.entity.js'; -import { IsOptional } from 'class-validator'; -import { Role } from '../common/enums/role.enum.js'; - -@Entity('users') -export class UserEntity extends BaseEntity { - @PrimaryGeneratedColumn('uuid') - id: string; - - @IsOptional() - @Column({ nullable: true }) - avatar: string; - - @Column() - full_name: string; - - @Column({ unique: true }) - phone_number: string; - - @IsOptional() - @Column({ nullable: true }) - address: string; - - @Column({ nullable: true, unique: true }) - email: string; - - @Column({ type: 'enum', enum: Role, default: 'CUSTOMER' }) - role: Role; - - @Column() - hash_password: string; - - @IsOptional() - @Column({ default: true }) - is_valid: boolean; - - @IsOptional() - @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) - create_at: Date; - - @IsOptional() - @OneToMany(() => BranchEntity, (branch) => branch.owner) - branches: Relation[]; -} +import { + Entity, + Column, + BaseEntity, + PrimaryGeneratedColumn, + OneToMany, + ManyToOne, + Relation, + JoinColumn, + CreateDateColumn, +} from 'typeorm'; +import { BranchEntity } from './branch.entity.js'; +import { IsOptional } from 'class-validator'; +import { Role } from '../common/enums/role.enum.js'; +import { ReceiptEntity } from './receipt.entity.js'; + +@Entity('users') +export class UserEntity extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @IsOptional() + @Column({ nullable: true }) + avatar: string; + + @Column() + full_name: string; + + @Column({ unique: true }) + phone_number: string; + + @IsOptional() + @Column({ nullable: true }) + address: string; + + @Column({ nullable: true, unique: true }) + email: string; + + @Column({ type: 'enum', enum: Role, default: 'CUSTOMER' }) + role: Role; + + @Column() + hash_password: string; + + @Column({ default: true }) + is_valid: boolean; + + @CreateDateColumn() + create_at: Date; + + @OneToMany(() => BranchEntity, (branch) => branch.owner) + branches: Relation[]; + + @OneToMany(() => ReceiptEntity, (receipt) => receipt.sender) + in_receipts: Relation[]; + + @OneToMany(() => ReceiptEntity, (receipt) => receipt.receiver) + out_receipts: Relation[]; +} diff --git a/backend/src/migrations/1729963419864-enum-role.ts b/backend/src/migrations/1729963419864-enum-role.ts deleted file mode 100644 index 7d8b0c1d4613f0b5d0df61e687d05314dae7d4b5..0000000000000000000000000000000000000000 --- a/backend/src/migrations/1729963419864-enum-role.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class EnumRole1729963419864 implements MigrationInterface { - name = 'EnumRole1729963419864' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "FK_a2cecd1a3531c0b041e29ba46e1"`); - await queryRunner.query(`ALTER TABLE "users" RENAME COLUMN "role_id" TO "role"`); - await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "role"`); - await queryRunner.query(`CREATE TYPE "public"."users_role_enum" AS ENUM('CUSTOMER', 'ADMIN', 'BRANCH_MANAGER', 'AREA_MANAGER', 'STAFF', 'SHIPPER')`); - await queryRunner.query(`ALTER TABLE "users" ADD "role" "public"."users_role_enum" NOT NULL DEFAULT 'CUSTOMER'`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "role"`); - await queryRunner.query(`DROP TYPE "public"."users_role_enum"`); - await queryRunner.query(`ALTER TABLE "users" ADD "role" uuid NOT NULL DEFAULT 'f3750930-48ab-4c30-8681-d50e68e2bda7'`); - await queryRunner.query(`ALTER TABLE "users" RENAME COLUMN "role" TO "role_id"`); - await queryRunner.query(`ALTER TABLE "users" ADD CONSTRAINT "FK_a2cecd1a3531c0b041e29ba46e1" FOREIGN KEY ("role_id") REFERENCES "role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); - } - -} diff --git a/backend/src/migrations/1730474673934-RefactorAll.ts b/backend/src/migrations/1730474673934-RefactorAll.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab6a4451efebcf7206bffd1de98df9ee6064989e --- /dev/null +++ b/backend/src/migrations/1730474673934-RefactorAll.ts @@ -0,0 +1,56 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class RefactorAll1730474673934 implements MigrationInterface { + name = 'RefactorAll1730474673934' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "feeds" ("id" SERIAL NOT NULL, "author_id" character varying, "image_url" character varying, "title" character varying NOT NULL, "description" character varying, "create_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_3dafbf766ecbb1eb2017732153f" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "public"."users_role_enum" AS ENUM('CUSTOMER', 'ADMIN', 'BRANCH_MANAGER', 'AREA_MANAGER', 'STAFF', 'SHIPPER')`); + await queryRunner.query(`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "avatar" character varying, "full_name" character varying NOT NULL, "phone_number" character varying NOT NULL, "address" character varying, "email" character varying, "role" "public"."users_role_enum" NOT NULL DEFAULT 'CUSTOMER', "hash_password" character varying NOT NULL, "is_valid" boolean NOT NULL DEFAULT true, "create_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_17d1817f241f10a3dbafb169fd2" UNIQUE ("phone_number"), CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "branches" ("id" character varying NOT NULL, "name" character varying NOT NULL, "location" character varying NOT NULL, "phone_number" character varying NOT NULL, "create_at" TIMESTAMP NOT NULL DEFAULT now(), "ownerId" uuid, CONSTRAINT "PK_7f37d3b42defea97f1df0d19535" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "public"."menu_items_item_type_enum" AS ENUM('monchinh', 'trangmieng', 'giaikhat', 'khac')`); + await queryRunner.query(`CREATE TABLE "menu_items" ("id" character varying NOT NULL, "item_name" character varying NOT NULL, "image_url" character varying, "item_type" "public"."menu_items_item_type_enum" NOT NULL DEFAULT 'khac', "description" character varying NOT NULL, "price" integer NOT NULL, "create_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_57e6188f929e5dc6919168620c8" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "branch_menu" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "branch_id" character varying NOT NULL, "menu_id" character varying NOT NULL, "description" character varying, "is_open" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_977becffe98bbc626a56031b9e7" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "public"."payments_payment_method_enum" AS ENUM('cash', 'card', 'online_payment')`); + await queryRunner.query(`CREATE TABLE "payments" ("id" SERIAL NOT NULL, "payment_method" "public"."payments_payment_method_enum" NOT NULL DEFAULT 'cash', "value" integer NOT NULL, CONSTRAINT "PK_197ab7af18c93fbb0c9b28b4a59" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "public"."orders_order_type_enum" AS ENUM('take_away', 'offline', 'online')`); + await queryRunner.query(`CREATE TYPE "public"."orders_order_status_enum" AS ENUM('pending', 'confirmed', 'preparing', 'delivering', 'done')`); + await queryRunner.query(`CREATE TABLE "orders" ("id" SERIAL NOT NULL, "customer_id" uuid, "branch_id" character varying NOT NULL, "staff_id" uuid, "table_number" integer, "total_value" integer NOT NULL, "create_at" TIMESTAMP NOT NULL DEFAULT now(), "order_type" "public"."orders_order_type_enum" NOT NULL DEFAULT 'online', "order_status" "public"."orders_order_status_enum" NOT NULL DEFAULT 'pending', "payment_id" integer, CONSTRAINT "REL_5b3e94bd2aedc184f9ad8c1043" UNIQUE ("payment_id"), CONSTRAINT "PK_710e2d4957aa5878dfe94e4ac2f" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "order_items" ("id" SERIAL NOT NULL, "order_id" integer NOT NULL, "branch_menu_id" uuid NOT NULL, "quantity" integer NOT NULL, "price" integer NOT NULL, CONSTRAINT "PK_005269d8574e6fac0493715c308" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "branches" ADD CONSTRAINT "FK_8c6ae9f9c654c4fac71bccbb7ed" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "branch_menu" ADD CONSTRAINT "FK_96fd74bed807987cf2ee5d8f168" FOREIGN KEY ("branch_id") REFERENCES "branches"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "branch_menu" ADD CONSTRAINT "FK_703aa953158d2e80f3fbb0eb9ea" FOREIGN KEY ("menu_id") REFERENCES "menu_items"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "orders" ADD CONSTRAINT "FK_772d0ce0473ac2ccfa26060dbe9" FOREIGN KEY ("customer_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "orders" ADD CONSTRAINT "FK_17b723da2c12837f4bc21e33398" FOREIGN KEY ("branch_id") REFERENCES "branches"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "orders" ADD CONSTRAINT "FK_40337bbb0e0cc7113dc3037fc60" FOREIGN KEY ("staff_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "orders" ADD CONSTRAINT "FK_5b3e94bd2aedc184f9ad8c10439" FOREIGN KEY ("payment_id") REFERENCES "payments"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "order_items" ADD CONSTRAINT "FK_145532db85752b29c57d2b7b1f1" FOREIGN KEY ("order_id") REFERENCES "orders"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "order_items" ADD CONSTRAINT "FK_927879f38b3098216737427d2f0" FOREIGN KEY ("branch_menu_id") REFERENCES "branch_menu"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "order_items" DROP CONSTRAINT "FK_927879f38b3098216737427d2f0"`); + await queryRunner.query(`ALTER TABLE "order_items" DROP CONSTRAINT "FK_145532db85752b29c57d2b7b1f1"`); + await queryRunner.query(`ALTER TABLE "orders" DROP CONSTRAINT "FK_5b3e94bd2aedc184f9ad8c10439"`); + await queryRunner.query(`ALTER TABLE "orders" DROP CONSTRAINT "FK_40337bbb0e0cc7113dc3037fc60"`); + await queryRunner.query(`ALTER TABLE "orders" DROP CONSTRAINT "FK_17b723da2c12837f4bc21e33398"`); + await queryRunner.query(`ALTER TABLE "orders" DROP CONSTRAINT "FK_772d0ce0473ac2ccfa26060dbe9"`); + await queryRunner.query(`ALTER TABLE "branch_menu" DROP CONSTRAINT "FK_703aa953158d2e80f3fbb0eb9ea"`); + await queryRunner.query(`ALTER TABLE "branch_menu" DROP CONSTRAINT "FK_96fd74bed807987cf2ee5d8f168"`); + await queryRunner.query(`ALTER TABLE "branches" DROP CONSTRAINT "FK_8c6ae9f9c654c4fac71bccbb7ed"`); + await queryRunner.query(`DROP TABLE "order_items"`); + await queryRunner.query(`DROP TABLE "orders"`); + await queryRunner.query(`DROP TYPE "public"."orders_order_status_enum"`); + await queryRunner.query(`DROP TYPE "public"."orders_order_type_enum"`); + await queryRunner.query(`DROP TABLE "payments"`); + await queryRunner.query(`DROP TYPE "public"."payments_payment_method_enum"`); + await queryRunner.query(`DROP TABLE "branch_menu"`); + await queryRunner.query(`DROP TABLE "menu_items"`); + await queryRunner.query(`DROP TYPE "public"."menu_items_item_type_enum"`); + await queryRunner.query(`DROP TABLE "branches"`); + await queryRunner.query(`DROP TABLE "users"`); + await queryRunner.query(`DROP TYPE "public"."users_role_enum"`); + await queryRunner.query(`DROP TABLE "feeds"`); + } + +} diff --git a/backend/src/migrations/1730547520878-AddReceipt.ts b/backend/src/migrations/1730547520878-AddReceipt.ts new file mode 100644 index 0000000000000000000000000000000000000000..dbc05b212caa21ec04d0c2b3ce1996e7f31c89fa --- /dev/null +++ b/backend/src/migrations/1730547520878-AddReceipt.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddReceipt1730547520878 implements MigrationInterface { + name = 'AddReceipt1730547520878' + + public async up(queryRunner: QueryRunner): Promise { + 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"))`); + await queryRunner.query(`ALTER TABLE "branches" ADD "image_url" character varying`); + await queryRunner.query(`ALTER TABLE "branch_menu" ADD "sold_count" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "branches" ALTER COLUMN "name" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "branches" ALTER COLUMN "location" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "branches" ALTER COLUMN "phone_number" DROP NOT NULL`); + 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`); + 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`); + 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`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "receipts" DROP CONSTRAINT "FK_366c3d3cf125da97552f40001a1"`); + await queryRunner.query(`ALTER TABLE "receipts" DROP CONSTRAINT "FK_eda4c4e486a25beef4dc82a41d5"`); + await queryRunner.query(`ALTER TABLE "receipts" DROP CONSTRAINT "FK_82e9dee911c0e7393154d1d98ad"`); + await queryRunner.query(`ALTER TABLE "branches" ALTER COLUMN "phone_number" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "branches" ALTER COLUMN "location" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "branches" ALTER COLUMN "name" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "branch_menu" DROP COLUMN "sold_count"`); + await queryRunner.query(`ALTER TABLE "branches" DROP COLUMN "image_url"`); + await queryRunner.query(`DROP TABLE "receipts"`); + } + +} diff --git a/backend/src/migrations/1730549959767-RemoveEnums.ts b/backend/src/migrations/1730549959767-RemoveEnums.ts new file mode 100644 index 0000000000000000000000000000000000000000..141ce953a4633eb2e8ac4cfca5f2f98c5bec8b10 --- /dev/null +++ b/backend/src/migrations/1730549959767-RemoveEnums.ts @@ -0,0 +1,36 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class RemoveEnums1730549959767 implements MigrationInterface { + name = 'RemoveEnums1730549959767' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "menu_items" DROP COLUMN "item_type"`); + await queryRunner.query(`DROP TYPE "public"."menu_items_item_type_enum"`); + await queryRunner.query(`ALTER TABLE "menu_items" ADD "item_type" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "payment_method"`); + await queryRunner.query(`DROP TYPE "public"."payments_payment_method_enum"`); + await queryRunner.query(`ALTER TABLE "payments" ADD "payment_method" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "order_type"`); + await queryRunner.query(`DROP TYPE "public"."orders_order_type_enum"`); + await queryRunner.query(`ALTER TABLE "orders" ADD "order_type" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "order_status"`); + await queryRunner.query(`DROP TYPE "public"."orders_order_status_enum"`); + await queryRunner.query(`ALTER TABLE "orders" ADD "order_status" integer NOT NULL DEFAULT '0'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "order_status"`); + await queryRunner.query(`CREATE TYPE "public"."orders_order_status_enum" AS ENUM('pending', 'confirmed', 'preparing', 'delivering', 'done')`); + await queryRunner.query(`ALTER TABLE "orders" ADD "order_status" "public"."orders_order_status_enum" NOT NULL DEFAULT 'pending'`); + await queryRunner.query(`ALTER TABLE "orders" DROP COLUMN "order_type"`); + await queryRunner.query(`CREATE TYPE "public"."orders_order_type_enum" AS ENUM('take_away', 'offline', 'online')`); + await queryRunner.query(`ALTER TABLE "orders" ADD "order_type" "public"."orders_order_type_enum" NOT NULL DEFAULT 'online'`); + await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "payment_method"`); + await queryRunner.query(`CREATE TYPE "public"."payments_payment_method_enum" AS ENUM('cash', 'card', 'online_payment')`); + await queryRunner.query(`ALTER TABLE "payments" ADD "payment_method" "public"."payments_payment_method_enum" NOT NULL DEFAULT 'cash'`); + await queryRunner.query(`ALTER TABLE "menu_items" DROP COLUMN "item_type"`); + await queryRunner.query(`CREATE TYPE "public"."menu_items_item_type_enum" AS ENUM('monchinh', 'trangmieng', 'giaikhat', 'khac')`); + await queryRunner.query(`ALTER TABLE "menu_items" ADD "item_type" "public"."menu_items_item_type_enum" NOT NULL DEFAULT 'khac'`); + } + +} diff --git a/backend/src/modules/branch-menus/branch-menus.controller.ts b/backend/src/modules/branch-menus/branch-menus.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..dcf4b64f8ead9938df72a3a4ea318f9461cea1d4 --- /dev/null +++ b/backend/src/modules/branch-menus/branch-menus.controller.ts @@ -0,0 +1,55 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, +} from '@nestjs/common'; +import { BranchMenusService } from './branch-menus.service.js'; +import { CreateBranchMenuDto } from './dto/create-branch-menu.dto.js'; +import { UpdateBranchMenuDto } from './dto/update-branch-menu.dto.js'; +import { Public } from '../authentication/authentication.decorator.js'; +import { Paginate, PaginateQuery } from 'nestjs-paginate'; + +@Public() +@Controller('branchs/:branchId/menus') +export class BranchMenusController { + constructor(private readonly branchMenusService: BranchMenusService) {} + + @Post() // thêm menu vào branch + create( + @Param('branchId') branchId: string, + @Body() createBranchMenuDto: CreateBranchMenuDto, + ) { + return this.branchMenusService.create(branchId, createBranchMenuDto); + } + + @Get() // lấy danh sách menu trong branch + findAll( + @Param('branchId') branchId: string, + @Paginate() query: PaginateQuery, + ) { + // console.log('branchId', branchId); + return this.branchMenusService.findAll(branchId, query); + } + + @Get(':id') // lấy một menu trong branch + findOne(@Param('branchId') branchId: string, @Param('id') id: string) { + return this.branchMenusService.findOne(branchId, id); + } + + @Patch(':id') + update( + @Param('id') id: string, + @Body() updateBranchMenuDto: UpdateBranchMenuDto, + ) { + return this.branchMenusService.update(+id, updateBranchMenuDto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.branchMenusService.remove(+id); + } +} diff --git a/backend/src/modules/branch-menus/branch-menus.module.ts b/backend/src/modules/branch-menus/branch-menus.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..7174dbd27107c0e793467ed5bac445d328a6e104 --- /dev/null +++ b/backend/src/modules/branch-menus/branch-menus.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { BranchMenusService } from './branch-menus.service.js'; +import { BranchMenusController } from './branch-menus.controller.js'; +import { BranchModule } from '../branch/branch.module.js'; +import { MenuItemModule } from '../menu-item/menu-item.module.js'; + +@Module({ + imports: [BranchModule, MenuItemModule], + controllers: [BranchMenusController], + providers: [BranchMenusService], + exports: [BranchMenusService], +}) +export class BranchMenusModule {} diff --git a/backend/src/modules/branch-menus/branch-menus.service.ts b/backend/src/modules/branch-menus/branch-menus.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..592ebe5ba06c9e36d52f1f8d91a7de422383478f --- /dev/null +++ b/backend/src/modules/branch-menus/branch-menus.service.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@nestjs/common'; +import { CreateBranchMenuDto } from './dto/create-branch-menu.dto.js'; +import { UpdateBranchMenuDto } from './dto/update-branch-menu.dto.js'; +import { BranchService } from '../branch/branch.service.js'; +import { MenuItemService } from '../menu-item/menu-item.service.js'; +import { BranchMenuEntity } from '../../entities/branch-menu.entity.js'; +import { paginate, PaginateConfig, PaginateQuery } from 'nestjs-paginate'; +import { isUUID } from 'class-validator'; + +@Injectable() +export class BranchMenusService { + constructor( + private readonly branchService: BranchService, + private readonly menuItemService: MenuItemService, + ) {} + async create(branchId: string, createBranchMenuDto: CreateBranchMenuDto) { + const branch = await this.branchService.getBranchOrError(branchId); + const menuItem = await this.menuItemService.getMenuItemOrError( + createBranchMenuDto.menu_id, + ); + if (createBranchMenuDto.description) { + return await BranchMenuEntity.create({ + ...createBranchMenuDto, + branch_id: branchId, + }).save(); + } else { + return await BranchMenuEntity.create({ + ...createBranchMenuDto, + branch_id: branchId, + description: menuItem.description, + }).save(); + } + } + + async findAll(branchId: string, query: PaginateQuery) { + const paginateConfig: PaginateConfig = { + sortableColumns: ['id', 'branch_id', 'menu_id', 'description'], + nullSort: 'last', + defaultSortBy: [['id', 'DESC']], + searchableColumns: ['description'], + filterableColumns: { + // price: [], + // item_type: [FilterOperator.EQ], + }, + }; + return paginate( + query, + BranchMenuEntity.createQueryBuilder('bm') + .leftJoinAndSelect('bm.menu_item', 'menu_item') + .where('bm.branch_id = :branchId', { branchId: branchId }), + paginateConfig, + ); + } + + async findOne(branchId: string, id: string) { + if (isUUID(id)) return await BranchMenuEntity.findOneBy({ id }); + else { + return await BranchMenuEntity.findOne({ + where: { branch_id: branchId, menu_id: id }, + relations: ['menu_item'], + }); + } + } + + update(id: number, updateBranchMenuDto: UpdateBranchMenuDto) { + return `This action updates a #${id} branchMenu`; + } + + remove(id: number) { + return `This action removes a #${id} branchMenu`; + } +} diff --git a/backend/src/modules/branch-menus/dto/create-branch-menu.dto.ts b/backend/src/modules/branch-menus/dto/create-branch-menu.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..412bdcf999e372e78e7b0639b29cc7085412791a --- /dev/null +++ b/backend/src/modules/branch-menus/dto/create-branch-menu.dto.ts @@ -0,0 +1,10 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class CreateBranchMenuDto { + @IsString() + menu_id: string; + + @IsString() + @IsOptional() + description?: string; +} diff --git a/backend/src/modules/branch-menus/dto/update-branch-menu.dto.ts b/backend/src/modules/branch-menus/dto/update-branch-menu.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..574d23d8ff5ceab855d9119aa3f06b13b995cd87 --- /dev/null +++ b/backend/src/modules/branch-menus/dto/update-branch-menu.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateBranchMenuDto } from './create-branch-menu.dto.js'; + +export class UpdateBranchMenuDto extends PartialType(CreateBranchMenuDto) {} diff --git a/backend/src/modules/branch/branch.controller.ts b/backend/src/modules/branch/branch.controller.ts index d940ee44f8e3ad3d55c9db9599c816208543813c..e01f0d156151dcaadc9f5ebabb4345fc279b9ab5 100644 --- a/backend/src/modules/branch/branch.controller.ts +++ b/backend/src/modules/branch/branch.controller.ts @@ -10,8 +10,10 @@ import { import { BranchService } from './branch.service.js'; import { CreateBranchDto } from './dto/create-branch.dto.js'; import { UpdateBranchDto } from './dto/update-branch.dto.js'; +import { Public } from '../authentication/authentication.decorator.js'; -@Controller('branch') +@Public() +@Controller('branchs') export class BranchController { constructor(private readonly branchService: BranchService) {} @@ -47,7 +49,5 @@ export class BranchController { async addMenuItemToBranch(@Param('id') id: string) {} @Get(':id/menu-items') - async getMenuItemWithBranchId(@Param('id') id: string) { - - } + async getMenuItemWithBranchId(@Param('id') id: string) {} } diff --git a/backend/src/modules/branch/branch.module.ts b/backend/src/modules/branch/branch.module.ts index 39ea15e0d49db80b0b144298493a819282a3fd58..693c0dffdde40bc2414c28be6d0d24f9bdc015de 100644 --- a/backend/src/modules/branch/branch.module.ts +++ b/backend/src/modules/branch/branch.module.ts @@ -5,5 +5,6 @@ import { BranchController } from './branch.controller.js'; @Module({ controllers: [BranchController], providers: [BranchService], + exports: [BranchService], }) export class BranchModule {} diff --git a/backend/src/modules/branch/branch.service.ts b/backend/src/modules/branch/branch.service.ts index 0b576094666b4f64d6659ce94dc8dfc68c88e08f..d48696c540b4b27dee127a86dd09ed6878a4602a 100644 --- a/backend/src/modules/branch/branch.service.ts +++ b/backend/src/modules/branch/branch.service.ts @@ -1,4 +1,8 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { CreateBranchDto } from './dto/create-branch.dto.js'; import { BranchEntity } from '../../entities/branch.entity.js'; import { Public } from '../authentication/authentication.decorator.js'; @@ -9,6 +13,10 @@ import { plainToClass } from 'class-transformer'; @Injectable() export class BranchService { async create(createBranchDto: CreateBranchDto) { + const branch = await BranchEntity.findOneBy({ id: createBranchDto.id }); + if (branch) { + throw new BadRequestException('Branch already exists'); + } return await BranchEntity.create({ ...createBranchDto }).save(); } @@ -21,9 +29,10 @@ export class BranchService { } async getBranchOrError(id: string) { + console.log(id); const branch = await BranchEntity.findOneBy({ id }); if (!branch) { - throw new NotFoundException('Menu item not found'); + throw new NotFoundException('Branch not found'); } return branch; } diff --git a/backend/src/modules/branch/dto/create-branch.dto.ts b/backend/src/modules/branch/dto/create-branch.dto.ts index 0c78c3d624ec28dd99d181dfdfb73de5bdd0d8d8..3f37a6ee23e1603a686ee0f762c8d0cf48b28b34 100644 --- a/backend/src/modules/branch/dto/create-branch.dto.ts +++ b/backend/src/modules/branch/dto/create-branch.dto.ts @@ -1,6 +1,9 @@ import { IsString } from 'class-validator'; export class CreateBranchDto { + @IsString() + id: string; + @IsString() name: string; diff --git a/backend/src/modules/menu-item/dto/create-menu-item.dto.ts b/backend/src/modules/menu-item/dto/create-menu-item.dto.ts index db76a8e9a7d13e9824e0f4eaedf17b2e042f6b6e..644d1a4c014268df8af036a77e9694f56a12efef 100644 --- a/backend/src/modules/menu-item/dto/create-menu-item.dto.ts +++ b/backend/src/modules/menu-item/dto/create-menu-item.dto.ts @@ -10,9 +10,9 @@ export class CreateMenuItemDto { @IsUrl() image_url: string; - @IsString() + @IsNumber() @IsOptional() - item_group_id?: string; + item_type?: number; @IsString() @IsOptional() diff --git a/backend/src/modules/menu-item/dto/update-menu-item.dto.ts b/backend/src/modules/menu-item/dto/update-menu-item.dto.ts index 63b6df8a82a53864aaff79e35e44b1e554c0899f..0fe0b1b1bd1a1e13c6a18ec285a6e211d4de5726 100644 --- a/backend/src/modules/menu-item/dto/update-menu-item.dto.ts +++ b/backend/src/modules/menu-item/dto/update-menu-item.dto.ts @@ -9,9 +9,9 @@ export class UpdateMenuItemDto { @IsOptional() image_url: string; - @IsString() + @IsNumber() @IsOptional() - item_group_id?: string; + item_type?: number; @IsString() @IsOptional() diff --git a/backend/src/modules/menu-item/menu-item.module.ts b/backend/src/modules/menu-item/menu-item.module.ts index c9095360fb1b853411ef4b9e38969b7ec9c3d376..5e441c537c44338ee81b40bd57ade2341f3fb29a 100644 --- a/backend/src/modules/menu-item/menu-item.module.ts +++ b/backend/src/modules/menu-item/menu-item.module.ts @@ -5,5 +5,6 @@ import { MenuItemController } from './menu-item.controller.js'; @Module({ controllers: [MenuItemController], providers: [MenuItemService], + exports: [MenuItemService], }) export class MenuItemModule {} diff --git a/backend/src/modules/order/dto/create-order.dto.ts b/backend/src/modules/order/dto/create-order.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..a52d983d5b2ce2a74d1516aec589327ba7132d73 --- /dev/null +++ b/backend/src/modules/order/dto/create-order.dto.ts @@ -0,0 +1,26 @@ +import { + IsArray, + IsEnum, + IsNumber, + IsObject, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; +import { OrderType } from '../../../common/enums/OrderType.enum.js'; +import { OrderItemsDto } from './order-items.dto.js'; +import { Type } from 'class-transformer'; + +export class CreateOrderDto { + @IsOptional() + @IsNumber() + table_number?: number; + + @IsNumber() + order_type: number; + + @IsArray() + @ValidateNested() + @Type(() => OrderItemsDto) + order_items: OrderItemsDto[]; +} diff --git a/backend/src/modules/order/dto/order-items.dto.ts b/backend/src/modules/order/dto/order-items.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..ebfb5ed4ceaf94e06c3e29eb0f7fa29681d98c06 --- /dev/null +++ b/backend/src/modules/order/dto/order-items.dto.ts @@ -0,0 +1,9 @@ +import { IsNumber, IsString } from 'class-validator'; + +export class OrderItemsDto { + @IsString() + menu_id: string; + + @IsNumber() + quantity: number; +} diff --git a/backend/src/modules/order/order.controller.ts b/backend/src/modules/order/order.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea2728d2d2676f874b3ccd93d7d02a1d46ae6b97 --- /dev/null +++ b/backend/src/modules/order/order.controller.ts @@ -0,0 +1,66 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Req, +} from '@nestjs/common'; +import { OrderService } from './order.service.js'; +import { CreateOrderDto } from './dto/create-order.dto.js'; +import { Role } from '../../common/enums/role.enum.js'; + +@Controller('branchs/:branchId/orders') +export class OrderController { + constructor(private readonly orderService: OrderService) {} + + @Post() + async create( + @Param('branchId') branchId: string, + @Req() req: Request, + @Body() createOrderDto: CreateOrderDto, + ) { + const userId = req['user'].sub; + const role = req['user'].roles; + console.log(req['user']); + if (role == Role.CUSTOMER) + return this.orderService.createFromCustomer( + branchId, + userId, + createOrderDto, + ); + else + return this.orderService.createFromStaff( + branchId, + userId, + createOrderDto, + ); + } + + @Get() + async findAll(@Req() req: Request) { + const userId = req['user'].sub; + console.log(req['user']); + return this.orderService.findAll(); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + return this.orderService.findOne(+id); + } + + // @Patch(':id') + // async update( + // @Param('id') id: string, + // @Body() updateOrderDto: UpdateOrderDto, + // ) { + // return this.orderService.update(+id, updateOrderDto); + // } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.orderService.remove(+id); + } +} diff --git a/backend/src/modules/order/order.module.ts b/backend/src/modules/order/order.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..dc7844ab9d70b00568cd7e7808c9af174bfafc47 --- /dev/null +++ b/backend/src/modules/order/order.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { OrderService } from './order.service.js'; +import { OrderController } from './order.controller.js'; +import { BranchModule } from '../branch/branch.module.js'; +import { BranchMenusModule } from '../branch-menus/branch-menus.module.js'; + +@Module({ + imports: [BranchModule, BranchMenusModule], + controllers: [OrderController], + providers: [OrderService], +}) +export class OrderModule {} diff --git a/backend/src/modules/order/order.service.ts b/backend/src/modules/order/order.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..7aa44ccfa60be1b829b21ad373dedf350e93ba58 --- /dev/null +++ b/backend/src/modules/order/order.service.ts @@ -0,0 +1,137 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { CreateOrderDto } from './dto/create-order.dto.js'; +import { UserEntity } from '../../entities/user.entity.js'; +import { OrderEntity } from '../../entities/order.entity.js'; +import { BranchService } from '../branch/branch.service.js'; +import { OrderType } from '../../common/enums/OrderType.enum.js'; +import { isUUID } from 'class-validator'; +import { OrderItemEntity } from '../../entities/order-item.entity.js'; +import { BranchMenuEntity } from '../../entities/branch-menu.entity.js'; +import { OrderStatus } from '../../common/enums/OrderStatus.enum.js'; + +@Injectable() +export class OrderService { + constructor(private readonly branchService: BranchService) {} + async createFromCustomer( + branchId: string, + userId: string, + createOrderDto: CreateOrderDto, + ) { + console.log('??'); + if (createOrderDto.order_type != OrderType.ONLINE) { + throw new BadRequestException('customer cannot create offline order'); + } + + const user = await UserEntity.findOneBy({ id: userId }); + if (!user) { + throw new BadRequestException('User not found'); + } + const branch = await this.branchService.getBranchOrError(branchId); + if (!branch) { + throw new BadRequestException('Branch not found'); + } + const order = OrderEntity.create(); + order.branch = branch; + order.customer = user; + order.order_type = createOrderDto.order_type; + order.order_status = OrderStatus.PENDING; + order.total_value = 0; + await order.save(); + + let orderItems: OrderItemEntity[] = []; + let totalValue = 0; + for (const item of createOrderDto.order_items) { + let branchMenu: BranchMenuEntity; + if (!isUUID(item.menu_id)) { + branchMenu = await BranchMenuEntity.findOne({ + where: { branch_id: branchId, menu_id: item.menu_id }, + relations: ['menu_item'], + }); + } else { + branchMenu = await BranchMenuEntity.findOne({ + where: { branch_id: branchId, id: item.menu_id }, + relations: ['menu_item'], + }); + } + if (!branchMenu) { + throw new BadRequestException('Item not found in branch menu'); + } + const orderItem = OrderItemEntity.create(); + orderItem.branch_menu = branchMenu; + orderItem.price = branchMenu.menu_item.price; + orderItem.quantity = item.quantity; + orderItem.order_id = order.id; + totalValue += orderItem.price * orderItem.quantity; + orderItems.push(orderItem); + } + await order.save(); + order.total_value = totalValue; + await OrderItemEntity.save(orderItems); + return { ...order, order_items: orderItems }; + } + + async createFromStaff( + branchId: string, + userId: string, + createOrderDto: CreateOrderDto, + ) { + if (createOrderDto.order_type == OrderType.ONLINE) { + throw new BadRequestException('staff cannot create online order'); + } + // staff + const staff = await UserEntity.findOneBy({ id: userId }); + const branch = await this.branchService.getBranchOrError(branchId); + const order = OrderEntity.create(); + + order.branch = branch; + order.staff = staff; + order.order_type = createOrderDto.order_type; + order.table_number = createOrderDto.table_number; + order.order_status = OrderStatus.PREPARING; + order.total_value = 0; + await order.save(); + + let orderItems: OrderItemEntity[] = []; + let totalValue = 0; + for (const item of createOrderDto.order_items) { + let branchMenu: BranchMenuEntity; + if (!isUUID(item.menu_id)) { + branchMenu = await BranchMenuEntity.findOne({ + where: { branch_id: branchId, menu_id: item.menu_id }, + relations: ['menu_item'], + }); + } else { + branchMenu = await BranchMenuEntity.findOne({ + where: { branch_id: branchId, id: item.menu_id }, + relations: ['menu_item'], + }); + } + if (!branchMenu) { + throw new BadRequestException('Item not found in branch menu'); + } + const orderItem = OrderItemEntity.create(); + orderItem.branch_menu = branchMenu; + orderItem.price = branchMenu.menu_item.price; + orderItem.quantity = item.quantity; + orderItem.order_id = order.id; + totalValue += orderItem.price * orderItem.quantity; + orderItems.push(orderItem); + } + await order.save(); + order.total_value = totalValue; + await OrderItemEntity.save(orderItems); + return { ...order, order_items: orderItems }; + } + + findAll() { + return `This action returns all order`; + } + + findOne(id: number) { + return `This action returns a #${id} order`; + } + + remove(id: number) { + return `This action removes a #${id} order`; + } +} diff --git a/frontend/.gitignore b/frontend/.gitignore index 4d29575de80483b005c29bfcac5061cd2f45313e..063ee8eeaa6c2c7d216ce2cb6825b38f3373d96e 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -21,3 +21,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +.env +note_dev.txt +object.json \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a2b4ba5692393a098c3a9ea33cce8c1f6f077157..a9ed1e7114a3b049903ef57778623cd4cab3a4a0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,10 +11,14 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.7", "bootstrap": "^5.3.3", + "js-cookie": "^3.0.5", + "jsonwebtoken": "^9.0.2", "react": "^18.3.1", "react-bootstrap": "^2.10.5", "react-dom": "^18.3.1", + "react-image-crop": "^11.0.7", "react-router-dom": "^6.27.0", "react-scripts": "5.0.1", "validator": "^13.12.0", @@ -5870,6 +5874,31 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -6402,6 +6431,12 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -7812,6 +7847,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -13187,6 +13231,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -13350,6 +13403,28 @@ "node": ">=0.10.0" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -13365,6 +13440,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -13513,6 +13609,42 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -13525,6 +13657,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -15955,6 +16093,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -16356,6 +16500,15 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "license": "MIT" }, + "node_modules/react-image-crop": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-11.0.7.tgz", + "integrity": "sha512-ZciKWHDYzmm366JDL18CbrVyjnjH0ojufGDmScfS4ZUqLHg4nm6ATY+K62C75W4ZRNt4Ii+tX0bSjNk9LQ2xzQ==", + "license": "ISC", + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index acb0e5a90ad87fc3e64eefcbaed15f4d871af0d5..7a3bd263976ce8986fc99b45f2a64c67b376a75c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,10 +6,14 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.7", "bootstrap": "^5.3.3", + "js-cookie": "^3.0.5", + "jsonwebtoken": "^9.0.2", "react": "^18.3.1", "react-bootstrap": "^2.10.5", "react-dom": "^18.3.1", + "react-image-crop": "^11.0.7", "react-router-dom": "^6.27.0", "react-scripts": "5.0.1", "validator": "^13.12.0", diff --git a/frontend/public/default_avatar.jpg b/frontend/public/default_avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3113c7fa002e57eb1f987603c82fed140b255008 Binary files /dev/null and b/frontend/public/default_avatar.jpg differ diff --git a/frontend/src/index.js b/frontend/src/index.js index 202896abf058aafc1bc117151a9d82b933298669..ccc95560ed73c179fdcb5c6c32488718a0a3adab 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -10,6 +10,8 @@ import LoginPage from './pages/LoginPage'; import RegisterPage from './pages/RegisterPage'; import NewsPage from './pages/NewsPage'; import MenuPage from './pages/MenuPage'; +import CartPage from './pages/CartPage'; +import UserInfoPage from './pages/UserInfoPage'; const router = createBrowserRouter([ { @@ -35,6 +37,16 @@ const router = createBrowserRouter([ { path: "/menu", element: + }, + { + path: "/cart", + element: , + errorElement: + }, + { + path: "/userinfo", + element: , + errorElement: } ]); diff --git a/frontend/src/molecules/AdminNavBar.js b/frontend/src/molecules/AdminNavBar.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/frontend/src/molecules/Navbar.js b/frontend/src/molecules/Navbar.js index 5ed24f74d31eb2f036bf3642fabf92e30b4269eb..43eee5d666d8b1759565346036f7e6ec19ec697b 100644 --- a/frontend/src/molecules/Navbar.js +++ b/frontend/src/molecules/Navbar.js @@ -1,23 +1,23 @@ -import Container from 'react-bootstrap/Container'; -import Nav from 'react-bootstrap/Nav'; -import Navbar from 'react-bootstrap/Navbar'; -import Button from 'react-bootstrap/Button'; -import { Stack } from 'react-bootstrap'; +import {Container, Nav, Navbar, Button, Stack} from 'react-bootstrap' import { useNavigate } from 'react-router-dom'; +import DataStorage from '../organisms/DataStorage'; export default function ANavbar() { const navigate = useNavigate(); - + function handleLogout() { - sessionStorage.setItem('isLoggedIn','false'); - sessionStorage.removeItem('username'); - sessionStorage.removeItem('cart'); + DataStorage.set('isLoggedIn','false'); + DataStorage.remove('accessToken'); + DataStorage.remove('role'); + DataStorage.remove('username'); + DataStorage.remove('cart'); + DataStorage.remove('expiryDate'); navigate('/'); } - let username = sessionStorage.getItem('username'); - let isLoggedIn = sessionStorage.getItem('isLoggedIn'); + let username = DataStorage.get('username'); + let isLoggedIn = DataStorage.get('isLoggedIn'); let userContent; if (isLoggedIn === 'true') { @@ -26,7 +26,9 @@ export default function ANavbar() { - {' '} + diff --git a/frontend/src/organisms/DataStorage.js b/frontend/src/organisms/DataStorage.js new file mode 100644 index 0000000000000000000000000000000000000000..572549ce46d290f84dfd0d2fca47b4602cd2229a --- /dev/null +++ b/frontend/src/organisms/DataStorage.js @@ -0,0 +1,91 @@ +import Cookies from 'js-cookie'; + +export default class DataStorage { + static storageMethod = process.env.REACT_APP_STORAGE_METHOD; + + // Get data + static get(key) { + if (this.storageMethod === 'session') { + return sessionStorage.getItem(key); + } else if (this.storageMethod === 'cookie') { + return Cookies.get(key); + } + return null; + } + + // Set data + static set(key, value, param = { expiryDate: null }) { + if (this.storageMethod === 'session') { + sessionStorage.setItem(key, value); + } else if (this.storageMethod === 'cookie') { + if (param.expiryDate) { + const expiryDate = new Date(param.expiryDate * 1000); + Cookies.set(key, value, { expires: expiryDate }) + } else if (Cookies.get('expiryDate')) { + const expiryDate = new Date(Cookies.get('expiryDate') * 1000); + Cookies.set(key, value, { expires: expiryDate }); + } else Cookies.set(key, value, { expires: 7 })// Expires in 7 by default + } + } + + // Remove data + static remove(key) { + if (this.storageMethod === 'session') { + sessionStorage.removeItem(key); + } else if (this.storageMethod === 'cookie') { + Cookies.remove(key); + } + } + + // Get all data + static getAll() { + const data = {}; + if (this.storageMethod === 'session') { + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + data[key] = sessionStorage.getItem(key); + } + } else if (this.storageMethod === 'cookie') { + const cookies = Cookies.get(); + Object.keys(cookies).forEach(key => { + data[key] = cookies[key]; + }); + } + return data; + } + + // Clear all data + static clearAll() { + if (this.storageMethod === 'session') { + sessionStorage.clear(); + } else if (this.storageMethod === 'cookie') { + const cookies = Cookies.get(); + Object.keys(cookies).forEach(key => { + Cookies.remove(key); + }); + } + } + + // Check if a key exists + static hasKey(key) { + if (this.storageMethod === 'session') { + return sessionStorage.getItem(key) !== null; + } else if (this.storageMethod === 'cookie') { + return Cookies.get(key) !== undefined; + } + return false; + } + + // Get all keys + static getKeys() { + const keys = []; + if (this.storageMethod === 'session') { + for (let i = 0; i < sessionStorage.length; i++) { + keys.push(sessionStorage.key(i)); + } + } else if (this.storageMethod === 'cookie') { + keys.push(...Object.keys(Cookies.get())); + } + return keys; + } +} diff --git a/frontend/src/organisms/MenuSection.js b/frontend/src/organisms/MenuSection.js index 4ca3bbe11b34bd924e13754e70675c8e7590768a..fd13469efac41edb4b60c9afa0f2fd14ea29b834 100644 --- a/frontend/src/organisms/MenuSection.js +++ b/frontend/src/organisms/MenuSection.js @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Container, Carousel, Row, Col } from 'react-bootstrap'; +import { Container, Carousel, Row, Col, Button } from 'react-bootstrap'; import MenuItem from '../molecules/MenuItem'; function MenuSection() { @@ -20,7 +20,7 @@ function MenuSection() { return (

Menu

- + {Array.from({ length: countCarouselSlides }).map((_, slideIndex) => (
@@ -33,7 +33,7 @@ function MenuSection() {
-
+
{menuItems.map((item, idx) => ( @@ -45,6 +45,7 @@ function MenuSection() { ))} +
diff --git a/frontend/src/organisms/jwtDecoder.js b/frontend/src/organisms/jwtDecoder.js new file mode 100644 index 0000000000000000000000000000000000000000..e822d8aee829a86d9c2a7b6aac71c6645b1af71c --- /dev/null +++ b/frontend/src/organisms/jwtDecoder.js @@ -0,0 +1,22 @@ +function decodeBase64Url(base64Url) { + // Thay các ký tự theo chuẩn Base64 URL thành chuẩn Base64 thông thường + let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + // Thêm padding nếu thiếu + base64 += '='.repeat((4 - base64.length % 4) % 4); + // Giải mã từ Base64 sang chuỗi JSON + return JSON.parse(atob(base64)); +} + +export default function jwtDecoder(jwtToken) { + const [header, payload, _] = jwtToken.split('.'); + + // Giải mã Header và Payload + const decodedHeader = decodeBase64Url(header); + const decodedPayload = decodeBase64Url(payload); + console.log("Signature:", _); + + console.log("Header:", decodedHeader); + console.log("Payload:", decodedPayload); + return {"header": decodedHeader, "payload": decodedPayload} +} + diff --git a/frontend/src/pages/AdminHomePage.js b/frontend/src/pages/AdminHomePage.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/frontend/src/pages/CartPage.js b/frontend/src/pages/CartPage.js new file mode 100644 index 0000000000000000000000000000000000000000..4bffb65fefe80e13d07eb5497b29a2e41dce1255 --- /dev/null +++ b/frontend/src/pages/CartPage.js @@ -0,0 +1,72 @@ +import BasicTemplate from "../templates/BasicTemplate"; +import { Container, Row, Col, Card, Button } from "react-bootstrap"; +import { useState, useEffect } from "react"; +import DataStorage from "../organisms/DataStorage"; + +export default function CartPage() { + + const [cartItems, setCartItems] = useState([]); + + useEffect(() => { + // Lấy giỏ hàng từ sessionStorage + const cart = JSON.parse(DataStorage.get('cart')) || {}; + + // Chuyển cart thành mảng chứa các món có số lượng > 0 + const items = Object.entries(cart) + .filter(([name, amount]) => amount > 0) + .map(([name, amount]) => ({ + name, + imageSrc: '/placeholder3.jpg', + price: 100, // Thêm đơn giá của món ăn (giả sử ở đây là 100 cho mỗi món) + amount + })); + + setCartItems(items); + }, []); + + + return ( + + {cartItems.length > 0 ? ( +
+

Giỏ hàng của bạn

+ + {cartItems.map((item, idx) => ( + + + + + + {item.name} + Đơn giá: {item.price} VND + Số lượng: {item.amount} + + Tổng cộng: {item.price * item.amount} VND + + + + + + ))} + + +
+ ) : ( +
+

Giỏ hàng của bạn hiện đang trống.

+ +
+ )} + + ) + } /> + ) +} \ No newline at end of file diff --git a/frontend/src/pages/LoginPage.js b/frontend/src/pages/LoginPage.js index 0e2f7a6ab5cb86e70d31553687c634fb7f1bbbc5..88ab274cf6081a828c17c6be95917516306a243b 100644 --- a/frontend/src/pages/LoginPage.js +++ b/frontend/src/pages/LoginPage.js @@ -2,9 +2,14 @@ import { useState } from "react"; import { Alert, Container, Form, Row, Col, Button, Card } from "react-bootstrap"; import BasicTemplate from "../templates/BasicTemplate"; import { useNavigate } from "react-router-dom"; +import axios from 'axios'; +import jwtDecoder from "../organisms/jwtDecoder"; +import DataStorage from "../organisms/DataStorage"; export default function LoginPage() { + const domain = process.env.REACT_APP_API_URL; + const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); @@ -16,16 +21,43 @@ export default function LoginPage() { // Validate password and confirm password match if (password.length === 0) { setError('Hãy nhập mật khẩu'); - } else if (username.length === 0) { + } else if (username.trim().length === 0) { setError('Tên đăng nhập không thể trống') } else { setError(''); - sessionStorage.setItem('username','testUser'); - sessionStorage.setItem('isLoggedIn','true'); - sessionStorage.setItem('cart','{}'); - // Xử lý submit form ở đây (ví dụ: gọi API đăng nhập) - console.log('Đăng nhập thành công', { username, password }); - navigate('/'); + + let data = { + 'username': username, + 'password': password + } + + axios.post(domain + '/authentication/login', data) + .then((response) => { + if (response.status === 200) { + // setError(JSON.stringify(jwtDecoder(response.data.access_token))); + const decodedToken = jwtDecoder(response.data.access_token); + const full_name = decodedToken.payload.username; + const role = decodedToken.payload.roles; + const expiryTime = decodedToken.payload.exp; + DataStorage.set('expiryDate', expiryTime, {expiryDate:expiryTime}); + DataStorage.set('username', full_name); + DataStorage.set('role', role); + DataStorage.set('isLoggedIn', 'true'); + DataStorage.set('accessToken', response.data.access_token); + DataStorage.set('cart', '{}'); + navigate('/'); + } else { + setError(JSON.stringify(response)); + } + }) + .catch((error) => { + if (error.status === 400) { + setError('Lỗi đăng nhập, vui lòng kiểm tra lại tài khoản và mật khẩu'); + } else if (error.status === 500) { + setError('Server đang tạm gặp vấn đề'); + } + }) + } }; diff --git a/frontend/src/pages/MenuPage.js b/frontend/src/pages/MenuPage.js index 7f5a2df0bc21fb3a9a2435fba0102575abb6f076..131c6f7a323ac5dd5ab812bfdef0be02c08fa5e9 100644 --- a/frontend/src/pages/MenuPage.js +++ b/frontend/src/pages/MenuPage.js @@ -1,16 +1,55 @@ import { useState } from 'react'; -import { Modal, Button, Container, Row, Col, Tab, Tabs } from 'react-bootstrap'; +import { Modal, Button, Container, Row, Col, Tab, Tabs, Form, InputGroup } from 'react-bootstrap'; import MenuItem from '../molecules/MenuItem'; import BasicTemplate from '../templates/BasicTemplate'; +import DataStorage from '../organisms/DataStorage'; + function MenuPage() { const [key, setKey] = useState('cat1'); const [selectedDish, setSelectedDish] = useState(null); const [show, setShow] = useState(false); + const [cartAmount, setCartAmount] = useState(0); + + function handleClose() { + const cart = JSON.parse(DataStorage.get('cart')) || {}; // Đảm bảo cart không null + cart[selectedDish.name] = cartAmount; + const filteredCart = Object.fromEntries( + Object.entries(cart).filter(([key, value]) => value > 0) + ); + DataStorage.set('cart', JSON.stringify(filteredCart)); + // console.log(JSON.stringify(filteredCart)); + setShow(false); + } + + function notLoggedInClose() { + setShow(false); + } - const handleClose = () => setShow(false); function handleShow(dish) { - setSelectedDish(dish); // Set the selected dish to state - setShow(true); // Show the modal + if (DataStorage.get('isLoggedIn') === null) { + setShow(true); + return; + } else { + setSelectedDish(dish); // Đặt selectedDish ngay lập tức + + // Sau khi cập nhật selectedDish, lấy dữ liệu từ cart theo tên món ăn mới + const cart = JSON.parse(DataStorage.get('cart')) || {}; + + // Kiểm tra số lượng món ăn trong cart + const amount = cart[dish.name] !== undefined ? cart[dish.name] : 0; + setCartAmount(amount); // Cập nhật số lượng + setShow(true); // Hiển thị modal + } + } + + function setIncrease() { + setCartAmount(cartAmount + 1); + } + + function setDecrease() { + if (cartAmount > 0) { + setCartAmount(cartAmount - 1); + } } const menuItems1 = [ @@ -35,23 +74,65 @@ function MenuPage() { { name: 'Món 6 thể loại 3', description: 'Mô tả món 3', imageSrc: '/placeholder3.jpg' } ]; + let modalContent; + + if (DataStorage.get('isLoggedIn') === null) { + modalContent = ( + + + Bạn chưa đăng nhập + + + Vui lòng đăng nhập để xem chi tiết món và đặt hàng + + + + + + + ) + } + else { + modalContent = ( + + {selectedDish?.name} {/* Dish name in the title */} + + + {selectedDish?.name} {/* Dish image */} +

{selectedDish?.description}

{/* Dish description */} + + + + + Số lượng + + - + setCartAmount(e.target.value)} /> + + + + + + +
+ + + +
); + } + return <> - - - {selectedDish?.name} {/* Dish name in the title */} - - -

{selectedDish?.description}

{/* Dish description */} - {selectedDish?.name} {/* Dish image */} -
- - - -
+ {modalContent}

Thực đơn

diff --git a/frontend/src/pages/RegisterPage.js b/frontend/src/pages/RegisterPage.js index 72c49ce2eca5648efea63338145c709a34dd6bab..05c840c7915baddac05d69f4660411fed138c712 100644 --- a/frontend/src/pages/RegisterPage.js +++ b/frontend/src/pages/RegisterPage.js @@ -19,7 +19,7 @@ const RegisterPage = () => { // Validate password and confirm password match if (full_name.length === 0) { setError('Họ và tên không thể để trống'); - } else if (!validator.isMobilePhone(phone_number)) { + } else if (!validator.isMobilePhone(phone_number, 'vi-VN')) { setError('Số điện thoại không hợp lệ'); } else if (!validator.isEmail(email)) { setError('Email không hợp lệ'); diff --git a/frontend/src/pages/UserInfoPage.js b/frontend/src/pages/UserInfoPage.js new file mode 100644 index 0000000000000000000000000000000000000000..01374e24244978fb56c06babd01be63fc62632ec --- /dev/null +++ b/frontend/src/pages/UserInfoPage.js @@ -0,0 +1,257 @@ +import BasicTemplate from "../templates/BasicTemplate"; +import { Container, Form, Row, Col, Card, Alert, Button, Image } from "react-bootstrap"; +import React, { useState, useEffect } from "react"; +import validator from "validator"; +import axios from "axios"; +import DataStorage from "../organisms/DataStorage"; + +export default function UserInfoPage() { + + // fetch data: + + const [error, setErrors] = useState(""); + const [initialData, setInitialData] = useState(null); + const [formData, setFormData] = useState({ + avatar: "/default_avatar.jpg", + full_name: "", + phone_number: "", + address: "", + email: "", + password: "", + confirmPassword: "", + }); + + useEffect(() => { + axios + .get(process.env.REACT_APP_API_URL + '/authentication/profile', { + headers: { + Authorization: `Bearer ${DataStorage.get('accessToken')}`, + }, + }) + .then((response) => { + if (response.status === 401) { + // phiên hết hạn + setErrors(JSON.stringify(response)); + } else { + const { avatar, full_name, address, phone_number, email } = response.data; + const fetchedData = { + avatar: avatar || "/default_avatar.jpg", + full_name: full_name || "", + phone_number: phone_number || "", + address: address || "", + email: email || "", + password: "", + confirmPassword: "", + }; + + if (fetchedData.full_name !== DataStorage.get('username')) DataStorage.set('username', fetchedData.full_name) + setInitialData(fetchedData); + setFormData(fetchedData); + } + }) + .catch((error) => { + setErrors(JSON.stringify(error)); + }); + }, []); + + // console.log('formdata', formData); + // console.log('initialdata', initialData); + // State để hiển thị lỗi khi validation + + const [isChanged, setChanged] = useState(false); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData({ ...formData, [name]: value }); + }; + + // Hàm xử lý thay đổi avatar + const handleAvatarChange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setFormData((prevData) => ({ + ...prevData, + avatar: reader.result // Cập nhật ảnh đại diện + })); + }; + reader.readAsDataURL(file); + setChanged(true); + } + }; + + useEffect(() => { + const hasChanges = () => { + if (initialData) { + for (let key in formData) { + if (formData[key] !== initialData[key]) { + return true; + } + } + return false; + } else { + return false; + } + }; + setChanged(hasChanges()); + }, [formData, initialData]); + + const handleSubmit = async (e) => { + e.preventDefault(); + setChanged(false); // prevent another click + if (formData.phone_number.trim() !== "" && !validator.isMobilePhone(formData.phone_number, 'vi-VN')) setErrors("Số điện thoại không hợp lệ"); + else if (formData.email.trim() !== "" && !validator.isEmail(formData.email)) setErrors("Email không hợp lệ"); + else if (formData.password !== "" && formData.confirmPassword !== formData.password) setErrors("Mật khẩu không khớp"); + else { + + // let avatarBase64 = null; + + // if (formData.avatar) { + // // Chuyển ảnh đại diện thành chuỗi Base64 + // avatarBase64 = await new Promise((resolve, reject) => { + // const reader = new FileReader(); + // reader.onloadend = () => resolve(reader.result.split(",")[1]); // Chỉ lấy phần Base64 + // reader.onerror = (error) => reject(error); + // reader.readAsDataURL(formData.avatar); + // }); + // } + + let data = {}; + + // if (avatarBase64) data.avatar = avatarBase64; + if (formData.full_name.trim() !== "" && formData.full_name.trim() !== initialData.full_name) data.full_name = formData.full_name.trim(); + if (formData.phone_number.trim() !== "" && formData.phone_number.trim() !== initialData.phone_number) data.phone_number = formData.phone_number.trim(); + if (formData.address.trim() !== "" && formData.address.trim() !== initialData.address) data.address = formData.address.trim(); + if (formData.email.trim() !== "" && formData.email.trim() !== initialData.email) data.email = formData.email.trim(); + if (formData.password.trim() !== "") data.hash_password = formData.password; + + // gửi request ở đây + axios.post(process.env.REACT_APP_API_URL + '/users/updateUser', data, { + headers: { + Authorization: `Bearer ${DataStorage.get('accessToken')}`, + } + }).then((response) => { + if (response.status === 200 || response.status === 201) { + window.location.reload(); // cập nhật lại thông tin + } else { + setErrors(JSON.stringify(response)); + } + }).catch((error) => setErrors(JSON.stringify(error))); + } + }; + + return + + + + + + Thông tin khách hàng + + + +
+ {error && {error}} + + + {/*Image*/} + User Avatar + + Ảnh đại diện + + + + + + Họ tên người dùng + + + + + + + Số điện thoại + + + + + Địa chỉ giao hàng mặc định + + + + + Email + + + + + Password + + + + + Nhập lại Password + + + + +
+
+
+ + + +
+ + + + ) + } /> +} \ No newline at end of file