87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/src/app/guest/menu/menu-order.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import Image from "next/image";
3 | import { Button } from "@/components/ui/button";
4 | import { useDishListQuery } from "@/queries/useDish";
5 | import Quantity from "./quantity";
6 | import { useState } from "react";
7 | import { GuestCreateOrdersBodyType } from "@/schemas/guest.schema";
8 | import { useGuestOrderMutation } from "@/queries/useGuest";
9 | import { useRouter } from "next/navigation";
10 | import { handleErrorApi } from "@/lib/utils";
11 |
12 | export default function MenuOrder() {
13 | const { data: listData } = useDishListQuery();
14 | const dishes = listData?.payload.data ?? [];
15 | const { mutateAsync } = useGuestOrderMutation();
16 | const [orders, setOrders] = useState([]);
17 | const totalPrice = dishes.reduce((result, dish) => {
18 | const order = orders.find((order) => order.dishId === dish.id);
19 | if (!order) return result;
20 | return result + order.quantity * dish.price;
21 | }, 0);
22 | const router = useRouter();
23 | const handleQuantityChange = (dishId: number, quantity: number) => {
24 | setOrders((prev) => {
25 | if (quantity === 0) {
26 | return prev.filter((order) => order.dishId !== dishId);
27 | }
28 | const index = prev.findIndex((order) => order.dishId === dishId);
29 | if (index === -1) {
30 | return [...prev, { dishId, quantity }];
31 | }
32 |
33 | const newOrders = [...prev];
34 | newOrders[index] = { ...newOrders[index], quantity };
35 | return newOrders;
36 | });
37 | };
38 |
39 | const handleOrder = async () => {
40 | try {
41 | await mutateAsync(orders);
42 | router.push(`/guest/orders`);
43 | } catch (error) {
44 | handleErrorApi({
45 | error,
46 | });
47 | }
48 | };
49 |
50 | return (
51 | <>
52 | {dishes
53 | .filter((item) => item.status !== "Hidden")
54 | .map((dish) => (
55 |
63 |
64 |
65 | {dish.status === "Unavailable" ? "Hết hàng" : ""}
66 |
67 |
77 |
78 |
79 | {dish.name}
80 | {dish.description}
81 |
82 | {dish.price.toLocaleString("VN")}đ
83 |
84 |
85 |
86 | handleQuantityChange(dish.id, value)}
88 | value={
89 | orders.find((order) => order.dishId === dish.id)?.quantity ??
90 | 0
91 | }
92 | />
93 |
94 |
95 | ))}
96 |
97 |
105 |
106 | >
107 | );
108 | }
109 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/src/app/(public)/(auth)/login/login-form.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | "use client";
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardHeader,
9 | CardTitle,
10 | } from "@/components/ui/card";
11 | import { Input } from "@/components/ui/input";
12 | import { Label } from "@/components/ui/label";
13 | import { useForm } from "react-hook-form";
14 | import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form";
15 | import { LoginBody, LoginBodyType } from "@/schemas/auth.schema";
16 | import { zodResolver } from "@hookform/resolvers/zod";
17 | import { useLoginMutation } from "@/queries/useAuth";
18 | import { toast } from "sonner";
19 | import { handleErrorApi } from "@/lib/utils";
20 | import { useRouter, useSearchParams } from "next/navigation";
21 | import { useEffect } from "react";
22 | import { useAppContext } from "@/components/app-provider";
23 |
24 | export default function LoginForm() {
25 | const loginMutation = useLoginMutation();
26 | const searchParams = useSearchParams();
27 | const clearTokens = searchParams.get("clearTokens");
28 | const { setRole } = useAppContext();
29 |
30 | const router = useRouter();
31 |
32 | useEffect(() => {
33 | if (clearTokens) setRole(undefined);
34 | }, [clearTokens, setRole]);
35 |
36 | const form = useForm({
37 | resolver: zodResolver(LoginBody),
38 | defaultValues: {
39 | email: "",
40 | password: "",
41 | },
42 | });
43 |
44 | const onSubmit = async (data: LoginBodyType) => {
45 | if (loginMutation.isPending) return;
46 | try {
47 | const result = await loginMutation.mutateAsync(data);
48 | toast.success(result.payload.message);
49 | setRole(result?.payload?.data?.account?.role);
50 | router.push("/manage/dashboard");
51 | } catch (error: any) {
52 | handleErrorApi({ error, setError: form.setError });
53 | }
54 | };
55 |
56 | return (
57 |
58 |
59 | Đăng nhập
60 |
61 | Nhập email và mật khẩu của bạn để đăng nhập vào hệ thống
62 |
63 |
64 |
65 |
121 |
122 |
123 |
124 | );
125 | }
126 |
--------------------------------------------------------------------------------
/src/schemas/account.schema.ts:
--------------------------------------------------------------------------------
1 | import { RoleValues } from '@/constants/type'
2 | import z from 'zod'
3 |
4 | export const AccountSchema = z.object({
5 | id: z.number(),
6 | name: z.string(),
7 | email: z.string(),
8 | role: z.string(),
9 | avatar: z.string().nullable()
10 | })
11 |
12 | export type AccountType = z.TypeOf
13 |
14 | export const AccountListRes = z.object({
15 | data: z.array(AccountSchema),
16 | message: z.string()
17 | })
18 |
19 | export type AccountListResType = z.TypeOf
20 |
21 | export const AccountRes = z
22 | .object({
23 | data: AccountSchema,
24 | message: z.string()
25 | })
26 | .strict()
27 |
28 | export type AccountResType = z.TypeOf
29 |
30 | export const CreateEmployeeAccountBody = z
31 | .object({
32 | name: z.string().trim().min(2).max(256),
33 | email: z.string().email(),
34 | avatar: z.string().url().optional(),
35 | password: z.string().min(6).max(100),
36 | confirmPassword: z.string().min(6).max(100)
37 | })
38 | .strict()
39 | .superRefine(({ confirmPassword, password }, ctx) => {
40 | if (confirmPassword !== password) {
41 | ctx.addIssue({
42 | code: 'custom',
43 | message: 'Mật khẩu không khớp',
44 | path: ['confirmPassword']
45 | })
46 | }
47 | })
48 |
49 | export type CreateEmployeeAccountBodyType = z.TypeOf
50 |
51 | export const UpdateEmployeeAccountBody = z
52 | .object({
53 | name: z.string().trim().min(2).max(256),
54 | email: z.string().email(),
55 | avatar: z.string().url().optional(),
56 | changePassword: z.boolean().optional(),
57 | password: z.string().min(6).max(100).optional(),
58 | confirmPassword: z.string().min(6).max(100).optional()
59 | })
60 | .strict()
61 | .superRefine(({ confirmPassword, password, changePassword }, ctx) => {
62 | if (changePassword) {
63 | if (!password || !confirmPassword) {
64 | ctx.addIssue({
65 | code: 'custom',
66 | message: 'Hãy nhập mật khẩu mới và xác nhận mật khẩu mới',
67 | path: ['changePassword']
68 | })
69 | } else if (confirmPassword !== password) {
70 | ctx.addIssue({
71 | code: 'custom',
72 | message: 'Mật khẩu không khớp',
73 | path: ['confirmPassword']
74 | })
75 | }
76 | }
77 | })
78 |
79 | export type UpdateEmployeeAccountBodyType = z.TypeOf
80 |
81 | export const UpdateMeBody = z
82 | .object({
83 | name: z.string().trim().min(2).max(256),
84 | avatar: z.string().url().optional()
85 | })
86 | .strict()
87 |
88 | export type UpdateMeBodyType = z.TypeOf
89 |
90 | export const ChangePasswordBody = z
91 | .object({
92 | oldPassword: z.string().min(6).max(100),
93 | password: z.string().min(6).max(100),
94 | confirmPassword: z.string().min(6).max(100)
95 | })
96 | .strict()
97 | .superRefine(({ confirmPassword, password }, ctx) => {
98 | if (confirmPassword !== password) {
99 | ctx.addIssue({
100 | code: 'custom',
101 | message: 'Mật khẩu mới không khớp',
102 | path: ['confirmPassword']
103 | })
104 | }
105 | })
106 |
107 | export type ChangePasswordBodyType = z.TypeOf
108 |
109 | export const AccountIdParam = z.object({
110 | id: z.coerce.number()
111 | })
112 |
113 | export type AccountIdParamType = z.TypeOf
114 |
115 | export const GetListGuestsRes = z.object({
116 | data: z.array(
117 | z.object({
118 | id: z.number(),
119 | name: z.string(),
120 | tableNumber: z.number().nullable(),
121 | createdAt: z.date(),
122 | updatedAt: z.date()
123 | })
124 | ),
125 | message: z.string()
126 | })
127 |
128 | export type GetListGuestsResType = z.TypeOf
129 |
130 | export const GetGuestListQueryParams = z.object({
131 | fromDate: z.coerce.date().optional(),
132 | toDate: z.coerce.date().optional()
133 | })
134 |
135 | export type GetGuestListQueryParamsType = z.TypeOf
136 |
137 | export const CreateGuestBody = z
138 | .object({
139 | name: z.string().trim().min(2).max(256),
140 | tableNumber: z.number()
141 | })
142 | .strict()
143 |
144 | export type CreateGuestBodyType = z.TypeOf
145 |
146 | export const CreateGuestRes = z.object({
147 | message: z.string(),
148 | data: z.object({
149 | id: z.number(),
150 | name: z.string(),
151 | role: z.enum(RoleValues),
152 | tableNumber: z.number().nullable(),
153 | createdAt: z.date(),
154 | updatedAt: z.date()
155 | })
156 | })
157 |
158 | export type CreateGuestResType = z.TypeOf
159 |
--------------------------------------------------------------------------------
/src/app/manage/setting/change-password-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
3 | import { Button } from '@/components/ui/button'
4 | import { Input } from '@/components/ui/input'
5 | import { Label } from '@/components/ui/label'
6 | import { useForm } from 'react-hook-form'
7 | import {
8 | ChangePasswordBody,
9 | ChangePasswordBodyType
10 | } from '@/schemas/account.schema'
11 | import { zodResolver } from '@hookform/resolvers/zod'
12 | import { Form, FormField, FormItem, FormMessage } from '@/components/ui/form'
13 | import { useChangePasswordMutation } from '@/queries/useAccount'
14 | import { toast } from 'sonner'
15 | import { handleErrorApi } from '@/lib/utils'
16 |
17 | export default function ChangePasswordForm() {
18 | const changePasswordMutation = useChangePasswordMutation()
19 | const form = useForm({
20 | resolver: zodResolver(ChangePasswordBody),
21 | defaultValues: {
22 | oldPassword: '',
23 | password: '',
24 | confirmPassword: ''
25 | }
26 | })
27 | const onSubmit = async (data: ChangePasswordBodyType) => {
28 | if(changePasswordMutation.isPending) return
29 | try {
30 | const result = await changePasswordMutation.mutateAsync(data)
31 | toast(result.payload.message)
32 | } catch (error) {
33 | handleErrorApi({error, setError: form.setError})
34 | }
35 | }
36 |
37 | return (
38 |
123 |
124 | )
125 | }
126 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from "react-hook-form"
14 |
15 | import { cn } from "@/lib/utils"
16 | import { Label } from "@/components/ui/label"
17 |
18 | const Form = FormProvider
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath
23 | > = {
24 | name: TName
25 | }
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue
29 | )
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext)
46 | const itemContext = React.useContext(FormItemContext)
47 | const { getFieldState, formState } = useFormContext()
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState)
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ")
53 | }
54 |
55 | const { id } = itemContext
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | }
65 | }
66 |
67 | type FormItemContextValue = {
68 | id: string
69 | }
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue
73 | )
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId()
80 |
81 | return (
82 |
83 |
84 |
85 | )
86 | })
87 | FormItem.displayName = "FormItem"
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField()
94 |
95 | return (
96 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
163 | {body}
164 |
165 | )
166 | })
167 | FormMessage.displayName = "FormMessage"
168 |
169 | export {
170 | useFormField,
171 | Form,
172 | FormItem,
173 | FormLabel,
174 | FormControl,
175 | FormDescription,
176 | FormMessage,
177 | FormField,
178 | }
179 |
--------------------------------------------------------------------------------
/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | }
142 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 |
68 |
69 | Close
70 |
71 |
72 | {/* title */}
73 | {React.Children.toArray(children).some(
74 | (child) => React.isValidElement(child) && child.type === SheetTitle
75 | ) ? null : (
76 | Sheet
77 | )}
78 | {children}
79 |
80 |
81 | ))
82 | SheetContent.displayName = SheetPrimitive.Content.displayName
83 |
84 | const SheetHeader = ({
85 | className,
86 | ...props
87 | }: React.HTMLAttributes) => (
88 |
95 | )
96 | SheetHeader.displayName = "SheetHeader"
97 |
98 | const SheetFooter = ({
99 | className,
100 | ...props
101 | }: React.HTMLAttributes) => (
102 |
109 | )
110 | SheetFooter.displayName = "SheetFooter"
111 |
112 | const SheetTitle = React.forwardRef<
113 | React.ElementRef,
114 | React.ComponentPropsWithoutRef
115 | >(({ className, ...props }, ref) => (
116 |
121 | ))
122 | SheetTitle.displayName = SheetPrimitive.Title.displayName
123 |
124 | const SheetDescription = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, ...props }, ref) => (
128 |
133 | ))
134 | SheetDescription.displayName = SheetPrimitive.Description.displayName
135 |
136 | export {
137 | Sheet,
138 | SheetPortal,
139 | SheetOverlay,
140 | SheetTrigger,
141 | SheetClose,
142 | SheetContent,
143 | SheetHeader,
144 | SheetFooter,
145 | SheetTitle,
146 | SheetDescription,
147 | }
148 |
--------------------------------------------------------------------------------
/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | const CommandDialog = ({ children, ...props }: DialogProps) => {
27 | return (
28 |
35 | )
36 | }
37 |
38 | const CommandInput = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
43 |
44 |
52 |
53 | ))
54 |
55 | CommandInput.displayName = CommandPrimitive.Input.displayName
56 |
57 | const CommandList = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
66 | ))
67 |
68 | CommandList.displayName = CommandPrimitive.List.displayName
69 |
70 | const CommandEmpty = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >((props, ref) => (
74 |
79 | ))
80 |
81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82 |
83 | const CommandGroup = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 |
97 | CommandGroup.displayName = CommandPrimitive.Group.displayName
98 |
99 | const CommandSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110 |
111 | const CommandItem = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
123 | ))
124 |
125 | CommandItem.displayName = CommandPrimitive.Item.displayName
126 |
127 | const CommandShortcut = ({
128 | className,
129 | ...props
130 | }: React.HTMLAttributes) => {
131 | return (
132 |
139 | )
140 | }
141 | CommandShortcut.displayName = "CommandShortcut"
142 |
143 | export {
144 | Command,
145 | CommandDialog,
146 | CommandInput,
147 | CommandList,
148 | CommandEmpty,
149 | CommandGroup,
150 | CommandItem,
151 | CommandShortcut,
152 | CommandSeparator,
153 | }
154 |
--------------------------------------------------------------------------------
/src/app/guest/orders/orders-cart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Badge } from "@/components/ui/badge";
4 | import { OrderStatus } from "@/constants/type";
5 | import socket from "@/lib/socket";
6 | import { formatCurrency, getVietnameseOrderStatus } from "@/lib/utils";
7 | import { useGuestGetOrderListMutation } from "@/queries/useGuest";
8 | import {
9 | PayGuestOrdersResType,
10 | UpdateOrderResType,
11 | } from "@/schemas/order.schema";
12 | import Image from "next/image";
13 | import { useEffect, useMemo } from "react";
14 | import { toast } from "sonner";
15 |
16 | export default function OrdersCart() {
17 | const { refetch, data } = useGuestGetOrderListMutation();
18 | const orders = useMemo(() => data?.payload.data ?? [], [data]);
19 |
20 | const { waitingForPaying, paid } = useMemo(() => {
21 | return orders.reduce(
22 | (result, order) => {
23 | if (
24 | order.status === OrderStatus.Delivered ||
25 | order.status === OrderStatus.Processing ||
26 | order.status === OrderStatus.Pending
27 | ) {
28 | return {
29 | ...result,
30 | waitingForPaying: {
31 | price:
32 | result.waitingForPaying.price +
33 | order.dishSnapshot.price * order.quantity,
34 | quantity: result.waitingForPaying.quantity + order.quantity,
35 | },
36 | };
37 | }
38 | if (order.status === OrderStatus.Paid) {
39 | return {
40 | ...result,
41 | paid: {
42 | price:
43 | result.paid.price + order.dishSnapshot.price * order.quantity,
44 | quantity: result.paid.quantity + order.quantity,
45 | },
46 | };
47 | }
48 | return result;
49 | },
50 | {
51 | waitingForPaying: {
52 | price: 0,
53 | quantity: 0,
54 | },
55 | paid: {
56 | price: 0,
57 | quantity: 0,
58 | },
59 | }
60 | );
61 | }, [orders]);
62 |
63 | useEffect(() => {
64 | if (socket.connected) {
65 | onConnect();
66 | }
67 |
68 | function onConnect() {
69 | console.log(socket.id);
70 | }
71 |
72 | function onDisconnect() {
73 | console.log("disconnect");
74 | }
75 |
76 | function onUpdateOrder(data: UpdateOrderResType["data"]) {
77 | console.log("update-order", data);
78 | const {
79 | dishSnapshot: { name },
80 | quantity,
81 | } = data;
82 | toast(
83 | `Món ${name} (SL: ${quantity}) vừa được cập nhật sang trạng thái "${getVietnameseOrderStatus(
84 | data.status
85 | )}"`
86 | );
87 | refetch();
88 | }
89 |
90 | function onPayment(data: PayGuestOrdersResType["data"]) {
91 | const { guest } = data[0];
92 | toast(
93 | `${guest?.name} tại bàn ${guest?.tableNumber} thanh toán thành công ${data.length} đơn`
94 | );
95 | refetch();
96 | }
97 |
98 | socket.on("update-order", onUpdateOrder);
99 | socket.on("payment", onPayment);
100 | socket.on("connect", onConnect);
101 | socket.on("disconnect", onDisconnect);
102 |
103 | return () => {
104 | socket.off("connect", onConnect);
105 | socket.off("disconnect", onDisconnect);
106 | socket.off("update-order", onUpdateOrder);
107 | socket.off("payment", onPayment);
108 | };
109 | }, [refetch]);
110 | return (
111 |
112 | {orders.map((order) => (
113 |
114 |
115 |
116 | {order.dishSnapshot.status === "Unavailable" ? "Hết hàng" : ""}
117 |
118 |
128 |
129 |
130 | {order.dishSnapshot.name}
131 |
132 | {order.dishSnapshot.price.toLocaleString("VN")}đ x
133 | {order.quantity}
134 |
135 |
136 |
137 |
138 | {getVietnameseOrderStatus(order.status)}
139 |
140 |
141 |
142 | ))}
143 |
144 | {paid.quantity !== 0 && (
145 |
146 |
147 | Đã thanh toán · {paid.quantity} món
148 | {formatCurrency(paid.price)}
149 |
150 |
151 | )}
152 |
153 |
154 | Chưa thanh toán · {waitingForPaying.quantity} món
155 | {formatCurrency(waitingForPaying.price)}
156 |
157 |
158 |
159 | );
160 | }
161 |
--------------------------------------------------------------------------------
/src/app/manage/tables/add-table.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Button } from '@/components/ui/button'
3 | import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
4 | import { Input } from '@/components/ui/input'
5 | import { Label } from '@/components/ui/label'
6 | import { zodResolver } from '@hookform/resolvers/zod'
7 | import { PlusCircle } from 'lucide-react'
8 | import { useState } from 'react'
9 | import { useForm } from 'react-hook-form'
10 | import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'
11 | import { getVietnameseTableStatus, handleErrorApi } from '@/lib/utils'
12 | import { CreateTableBody, CreateTableBodyType } from '@/schemas/table.schema'
13 | import { TableStatus, TableStatusValues } from '@/constants/type'
14 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
15 | import { useCreateTableMutation } from '@/queries/useTable'
16 | import { toast } from 'sonner'
17 |
18 | export default function AddTable() {
19 | const [open, setOpen] = useState(false)
20 | const form = useForm({
21 | resolver: zodResolver(CreateTableBody),
22 | defaultValues: {
23 | number: 0,
24 | capacity: 2,
25 | status: TableStatus.Hidden
26 | }
27 | })
28 | const createTableMutation = useCreateTableMutation()
29 |
30 | const reset = () => {
31 | form.reset()
32 | }
33 |
34 | const onSubmit = async (values: CreateTableBodyType) => {
35 | if(createTableMutation.isPending) return
36 | try {
37 | const result = await createTableMutation.mutateAsync(values)
38 | toast.success('You have created new dish successfully')
39 | reset()
40 | setOpen(false)
41 | } catch (error) {
42 | handleErrorApi({
43 | error,
44 | setError: form.setError
45 | })
46 | }
47 | }
48 |
49 | return (
50 |
142 | )
143 | }
144 |
--------------------------------------------------------------------------------
/src/lib/http.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 |
3 | import envConfig from "@/config";
4 | import { LoginResType } from "@/schemas/auth.schema";
5 | import { redirect } from "next/navigation";
6 | import {
7 | getAccessTokenFromLocalStorage,
8 | removeTokensFromLocalStorage,
9 | setAccessTokenToLocalStorage,
10 | setRefreshTokenToLocalStorage,
11 | } from "./utils";
12 |
13 | type CustomOptions = Omit & {
14 | baseUrl?: string | undefined;
15 | };
16 |
17 | const ENTITY_ERROR_STATUS = 422;
18 | const AUTHENTICATION_ERROR_STATUS = 401;
19 |
20 | type EntityErrorPayload = {
21 | message: string;
22 | errors: {
23 | field: string;
24 | message: string;
25 | }[];
26 | };
27 |
28 | export class HttpError extends Error {
29 | status: number;
30 | payload: {
31 | message: string;
32 | [key: string]: any;
33 | };
34 | constructor({
35 | status,
36 | payload,
37 | message = "HTTP Error",
38 | }: {
39 | status: number;
40 | payload: any;
41 | message?: string;
42 | }) {
43 | super(message);
44 | this.status = status;
45 | this.payload = payload;
46 | }
47 | }
48 |
49 | export class EntityError extends HttpError {
50 | status: typeof ENTITY_ERROR_STATUS;
51 | payload: EntityErrorPayload;
52 | constructor({
53 | status,
54 | payload,
55 | }: {
56 | status: typeof ENTITY_ERROR_STATUS;
57 | payload: EntityErrorPayload;
58 | }) {
59 | super({ status, payload, message: "Entity Error" });
60 | this.status = status;
61 | this.payload = payload;
62 | }
63 | }
64 |
65 | const isClient = typeof window !== "undefined";
66 |
67 | let clientLogoutRequest: null | Promise = null;
68 |
69 | const request = async (
70 | method: "GET" | "POST" | "PUT" | "DELETE",
71 | url: string,
72 | options?: CustomOptions | undefined
73 | ) => {
74 | let body: FormData | string | undefined = undefined;
75 |
76 | if (options?.body instanceof FormData) {
77 | body = options.body;
78 | } else if (options?.body) {
79 | body = JSON.stringify(options.body);
80 | }
81 |
82 | const baseHeaders: {
83 | [key: string]: string;
84 | } =
85 | body instanceof FormData
86 | ? {}
87 | : {
88 | "Content-Type": "application/json",
89 | };
90 |
91 | if (isClient) {
92 | const accessToken = getAccessTokenFromLocalStorage();
93 | if (accessToken) {
94 | baseHeaders.Authorization = `Bearer ${accessToken}`;
95 | }
96 | }
97 | const baseUrl =
98 | options?.baseUrl === undefined
99 | ? envConfig.NEXT_PUBLIC_API_ENDPOINT
100 | : options.baseUrl;
101 |
102 | const fullUrl = url.startsWith("/")
103 | ? `${baseUrl}${url}`
104 | : `${baseUrl}/${url}`;
105 |
106 | const res = await fetch(fullUrl, {
107 | ...options,
108 | headers: {
109 | ...baseHeaders,
110 | ...options?.headers,
111 | },
112 | body,
113 | method,
114 | });
115 | const payload: Response = await res.json();
116 | const data = {
117 | status: res.status,
118 | payload,
119 | };
120 | //_____________interceptor_______________//
121 | if (!res.ok) {
122 | if (res.status === ENTITY_ERROR_STATUS) {
123 | throw new EntityError(
124 | data as {
125 | status: 422;
126 | payload: EntityErrorPayload;
127 | }
128 | );
129 | //handle 401 token is expired
130 | } else if (res.status === AUTHENTICATION_ERROR_STATUS) {
131 | if (isClient) {
132 | if (!clientLogoutRequest) {
133 | clientLogoutRequest = fetch("/api/auth/logout", {
134 | method: "POST",
135 | body: null, //Logout cho phep luon luon thanh cong
136 | headers: {
137 | ...baseHeaders,
138 | } as any,
139 | });
140 |
141 | try {
142 | await clientLogoutRequest;
143 | } catch (error) {
144 | console.log(error);
145 | } finally {
146 | removeTokensFromLocalStorage();
147 | clientLogoutRequest = null;
148 | //redirect to login can make infinite loop error
149 | // location.href = '/login'
150 | }
151 | }
152 | } else {
153 | //access token is valid (calling on route handler | server components) but server is error 401
154 | const accessToken = (options?.headers as any)?.Authorization.split(
155 | " "
156 | )[1];
157 | console.log("run on it");
158 | redirect(`/logout?accessToken=${accessToken}`);
159 | }
160 | } else {
161 | throw new HttpError(data);
162 | }
163 | }
164 |
165 | if (isClient) {
166 | if (["/api/auth/login", "/api/guest/auth/login"].includes(url)) {
167 | const { accessToken, refreshToken } = (payload as LoginResType).data;
168 | setAccessTokenToLocalStorage(accessToken);
169 | setRefreshTokenToLocalStorage(refreshToken);
170 | } else if (["/api/auth/logout", "/api/guest/auth/logout"].includes(url)) {
171 | removeTokensFromLocalStorage();
172 | }
173 | }
174 |
175 | return data;
176 | };
177 |
178 | const http = {
179 | get(
180 | url: string,
181 | options?: Omit | undefined
182 | ) {
183 | return request("GET", url, options);
184 | },
185 |
186 | post(
187 | url: string,
188 | body: any,
189 | options?: Omit | undefined
190 | ) {
191 | return request("POST", url, { ...options, body });
192 | },
193 |
194 | put(
195 | url: string,
196 | body: any,
197 | options?: Omit | undefined
198 | ) {
199 | return request("PUT", url, { ...options, body });
200 | },
201 |
202 | delete(
203 | url: string,
204 | options?: Omit | undefined
205 | ) {
206 | return request("DELETE", url, { ...options });
207 | },
208 | };
209 |
210 | export default http;
211 |
--------------------------------------------------------------------------------
/src/components/auto-pagination.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button'
2 | import {
3 | Pagination,
4 | PaginationContent,
5 | PaginationEllipsis,
6 | PaginationItem,
7 | PaginationLink,
8 | PaginationNext,
9 | PaginationPrevious
10 | } from '@/components/ui/pagination'
11 | import { cn } from '@/lib/utils'
12 | import { ChevronLeft, ChevronRight } from 'lucide-react'
13 | interface Props {
14 | page: number
15 | pageSize: number
16 | pathname?: string
17 | isLink?: boolean
18 | onClick?: (pageNumber: number) => void
19 | }
20 |
21 | /**
22 | Với range = 2 áp dụng cho khoảng cách đầu, cuối và xung quanh current_page
23 |
24 | [1] 2 3 ... 19 20
25 | 1 [2] 3 4 ... 19 20
26 | 1 2 [3] 4 5 ... 19 20
27 | 1 2 3 [4] 5 6 ... 19 20
28 | 1 2 3 4 [5] 6 7 ... 19 20
29 |
30 | 1 2 ... 4 5 [6] 8 9 ... 19 20
31 |
32 | 1 2 ...13 14 [15] 16 17 ... 19 20
33 |
34 |
35 | 1 2 ... 14 15 [16] 17 18 19 20
36 | 1 2 ... 15 16 [17] 18 19 20
37 | 1 2 ... 16 17 [18] 19 20
38 | 1 2 ... 17 18 [19] 20
39 | 1 2 ... 18 19 [20]
40 | */
41 |
42 | const RANGE = 2
43 | export default function AutoPagination({
44 | page,
45 | pageSize,
46 | pathname = '/',
47 | isLink = true,
48 | onClick = (pageNumber) => {}
49 | }: Props) {
50 | const renderPagination = () => {
51 | let dotAfter = false
52 | let dotBefore = false
53 | const renderDotBefore = (index: number) => {
54 | if (!dotBefore) {
55 | dotBefore = true
56 | return (
57 |
58 |
59 |
60 | )
61 | }
62 | return null
63 | }
64 | const renderDotAfter = (index: number) => {
65 | if (!dotAfter) {
66 | dotAfter = true
67 | return (
68 |
69 |
70 |
71 | )
72 | }
73 | return null
74 | }
75 | return Array(pageSize)
76 | .fill(0)
77 | .map((_, index) => {
78 | const pageNumber = index + 1
79 |
80 | // Điều kiện để return về ...
81 | if (
82 | page <= RANGE * 2 + 1 &&
83 | pageNumber > page + RANGE &&
84 | pageNumber < pageSize - RANGE + 1
85 | ) {
86 | return renderDotAfter(index)
87 | } else if (page > RANGE * 2 + 1 && page < pageSize - RANGE * 2) {
88 | if (pageNumber < page - RANGE && pageNumber > RANGE) {
89 | return renderDotBefore(index)
90 | } else if (
91 | pageNumber > page + RANGE &&
92 | pageNumber < pageSize - RANGE + 1
93 | ) {
94 | return renderDotAfter(index)
95 | }
96 | } else if (
97 | page >= pageSize - RANGE * 2 &&
98 | pageNumber > RANGE &&
99 | pageNumber < page - RANGE
100 | ) {
101 | return renderDotBefore(index)
102 | }
103 | return (
104 |
105 | {isLink && (
106 |
115 | {pageNumber}
116 |
117 | )}
118 | {!isLink && (
119 |
128 | )}
129 |
130 | )
131 | })
132 | }
133 | return (
134 |
135 |
136 |
137 | {isLink && (
138 | {
149 | if (page === 1) {
150 | e.preventDefault()
151 | }
152 | }}
153 | />
154 | )}
155 | {!isLink && (
156 |
166 | )}
167 |
168 | {renderPagination()}
169 |
170 |
171 | {isLink && (
172 | {
183 | if (page === pageSize) {
184 | e.preventDefault()
185 | }
186 | }}
187 | />
188 | )}
189 | {!isLink && (
190 |
200 | )}
201 |
202 |
203 |
204 | )
205 | }
206 |
--------------------------------------------------------------------------------
/src/app/manage/setting/update-profile-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
3 | import { Button } from '@/components/ui/button'
4 | import { Input } from '@/components/ui/input'
5 | import { Label } from '@/components/ui/label'
6 | import { Upload } from 'lucide-react'
7 | import { useForm } from 'react-hook-form'
8 | import {
9 | UpdateMeBody,
10 | UpdateMeBodyType
11 | } from '@/schemas/account.schema'
12 | import { zodResolver } from '@hookform/resolvers/zod'
13 | import { Form, FormField, FormItem, FormMessage } from '@/components/ui/form'
14 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
15 | import { useEffect, useMemo, useRef, useState } from 'react'
16 | import { useAccountProfile, useUpdatePersonalProfileMutation } from '@/queries/useAccount'
17 | import { useUploadMediaMutation } from '@/queries/useMedia'
18 | import { toast } from 'sonner'
19 | import { handleErrorApi } from '@/lib/utils'
20 |
21 | export default function UpdateProfileForm() {
22 | const fileRef = useRef(null)
23 | const [file, setFile] = useState(null)
24 | const {data, refetch} = useAccountProfile()
25 | const updateProfileMutation = useUpdatePersonalProfileMutation()
26 | const uploadAvtMutation = useUploadMediaMutation()
27 |
28 | //useform
29 | const form = useForm({
30 | resolver: zodResolver(UpdateMeBody),
31 | defaultValues: {
32 | name: '',
33 | avatar: undefined
34 | }
35 | })
36 |
37 | const avatar = form.watch('avatar')
38 | const name = form.watch('name')
39 |
40 | useEffect(() => {
41 | if(data) {
42 | const { name, avatar } = data?.payload.data
43 | form.reset({
44 | name,
45 | avatar: avatar ?? undefined
46 | })
47 | }
48 | }, [form, data])
49 |
50 | const previewAvatar = useMemo(() => {
51 | return file ? URL.createObjectURL(file) : avatar
52 | }, [file, avatar])
53 |
54 | const reset = () => {
55 | form.reset()
56 | setFile(null)
57 | }
58 |
59 | const onSubmit = async (values: UpdateMeBodyType) => {
60 | // console.log('values', values)
61 | if(updateProfileMutation.isPending) return
62 | try {
63 | let body = values
64 | if(file) {
65 | const formData = new FormData()
66 | formData.append('file', file as Blob)
67 | const uploadAvtResult = await uploadAvtMutation.mutateAsync(formData)
68 | const imageUrl = uploadAvtResult.payload.data
69 | body = {
70 | ...values,
71 | avatar: imageUrl
72 | }
73 | }
74 | const result = await updateProfileMutation.mutateAsync(body)
75 | toast.success("Update personal profile successfully")
76 | refetch()
77 | } catch (error) {
78 | handleErrorApi({
79 | error,
80 | setError: form.setError
81 | })
82 | }
83 | }
84 |
85 | return (
86 |
166 |
167 | )
168 | }
169 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 | {children}
132 |
133 | ))
134 | SelectItem.displayName = SelectPrimitive.Item.displayName
135 |
136 | const SelectSeparator = React.forwardRef<
137 | React.ElementRef,
138 | React.ComponentPropsWithoutRef
139 | >(({ className, ...props }, ref) => (
140 |
145 | ))
146 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
147 |
148 | export {
149 | Select,
150 | SelectGroup,
151 | SelectValue,
152 | SelectTrigger,
153 | SelectContent,
154 | SelectLabel,
155 | SelectItem,
156 | SelectSeparator,
157 | SelectScrollUpButton,
158 | SelectScrollDownButton,
159 | }
160 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { UseFormSetError } from "react-hook-form";
3 | import { twMerge } from "tailwind-merge";
4 | import { EntityError } from "./http";
5 | import { toast } from "sonner";
6 | import jwt from "jsonwebtoken";
7 | import authApi from "@/apis/auth";
8 | import { DishStatus, OrderStatus, Role, TableStatus } from "@/constants/type";
9 | import envConfig from "@/config";
10 | import { TokenPayload } from "@/types/jwt.types";
11 | import guestApi from "@/apis/guest";
12 | import { format } from "date-fns";
13 | import { BookX, CookingPot, HandCoins, Loader, Truck } from "lucide-react";
14 |
15 | export function cn(...inputs: ClassValue[]) {
16 | return twMerge(clsx(inputs));
17 | }
18 |
19 | export const handleErrorApi = ({
20 | error,
21 | setError,
22 | }: {
23 | error: any;
24 | setError?: UseFormSetError;
25 | }) => {
26 | if (error instanceof EntityError && setError) {
27 | error.payload.errors.forEach((item) => {
28 | setError(item.field, {
29 | type: "server",
30 | message: item.message,
31 | });
32 | });
33 | } else {
34 | toast(error?.payload?.message ?? "Error is not known");
35 | }
36 | };
37 |
38 | const isBrowser = typeof window !== "undefined";
39 |
40 | export const getAccessTokenFromLocalStorage = () =>
41 | isBrowser ? localStorage.getItem("accessToken") : null;
42 |
43 | export const getRefreshTokenFromLocalStorage = () =>
44 | isBrowser ? localStorage.getItem("refreshToken") : null;
45 |
46 | export const setAccessTokenToLocalStorage = (value: string) =>
47 | isBrowser ? localStorage.setItem("accessToken", value) : null;
48 |
49 | export const setRefreshTokenToLocalStorage = (value: string) =>
50 | isBrowser ? localStorage.setItem("refreshToken", value) : null;
51 |
52 | export const removeTokensFromLocalStorage = () => {
53 | isBrowser && localStorage.removeItem("accessToken");
54 | isBrowser && localStorage.removeItem("refreshToken");
55 | };
56 |
57 | export const checkAndRefreshToken = async (param?: {
58 | onError?: () => void;
59 | onSuccess?: () => void;
60 | }) => {
61 | //check xem token còn hạn ko,
62 | // ko nên đưa ra khỏi hàm.
63 | const accessToken = getAccessTokenFromLocalStorage();
64 | const refreshToken = getRefreshTokenFromLocalStorage();
65 | if (!accessToken || !refreshToken) return;
66 |
67 | const decodeAccessToken = decodeToken(accessToken);
68 |
69 | const decodeRefreshToken = decodeToken(refreshToken);
70 | //exp của token tính theo giây, new Date().getTime() thì ms
71 | //set cookie bi lech, vi the tru hao 1s
72 | const now = new Date().getTime() / 1000 - 1;
73 |
74 | //refreshToken is expire then logout
75 | if (decodeRefreshToken.exp <= now) {
76 | removeTokensFromLocalStorage();
77 | return param?.onError && param.onError();
78 | }
79 | // example if access token has expire time that is 1h
80 | // check if there is 1/3 time (20p) left then refresh-token again
81 | // **leftTime = decodeAccessToken.exp - now
82 | // **expTime = decodeAccessToken.exp - decodeAccessToken.iat
83 | if (
84 | decodeAccessToken.exp - now <
85 | (decodeAccessToken.exp - decodeAccessToken.iat) / 3
86 | ) {
87 | //call refreshtoken api
88 | try {
89 | const role = decodeRefreshToken.role;
90 | const result =
91 | role === Role.Guest
92 | ? await guestApi.refreshToken()
93 | : await authApi.refreshToken();
94 | // console.log("####", result.payload.data.accessToken);
95 | setAccessTokenToLocalStorage(result.payload.data.accessToken);
96 | setRefreshTokenToLocalStorage(result.payload.data.refreshToken);
97 | param?.onSuccess && param.onSuccess();
98 | } catch (error) {
99 | param?.onError && param.onError();
100 | }
101 | }
102 | };
103 |
104 | export const formatCurrency = (number: number) => {
105 | return new Intl.NumberFormat("vi-VN", {
106 | style: "currency",
107 | currency: "VND",
108 | }).format(number);
109 | };
110 |
111 | export const getVietnameseDishStatus = (
112 | status: (typeof DishStatus)[keyof typeof DishStatus]
113 | ) => {
114 | switch (status) {
115 | case DishStatus.Available:
116 | return "Có sẵn";
117 | case DishStatus.Unavailable:
118 | return "Không có sẵn";
119 | default:
120 | return "Ẩn";
121 | }
122 | };
123 |
124 | export const getVietnameseOrderStatus = (
125 | status: (typeof OrderStatus)[keyof typeof OrderStatus]
126 | ) => {
127 | switch (status) {
128 | case OrderStatus.Delivered:
129 | return "Đã phục vụ";
130 | case OrderStatus.Paid:
131 | return "Đã thanh toán";
132 | case OrderStatus.Pending:
133 | return "Chờ xử lý";
134 | case OrderStatus.Processing:
135 | return "Đang nấu";
136 | default:
137 | return "Từ chối";
138 | }
139 | };
140 |
141 | export const getVietnameseTableStatus = (
142 | status: (typeof TableStatus)[keyof typeof TableStatus]
143 | ) => {
144 | switch (status) {
145 | case TableStatus.Available:
146 | return "Có sẵn";
147 | case TableStatus.Reserved:
148 | return "Đã đặt";
149 | default:
150 | return "Ẩn";
151 | }
152 | };
153 |
154 | export const getTableLink = ({
155 | token,
156 | tableNumber,
157 | }: {
158 | token: string;
159 | tableNumber: number;
160 | }) => {
161 | return (
162 | envConfig.NEXT_PUBLIC_URL + "/tables/" + tableNumber + "?token=" + token
163 | );
164 | };
165 |
166 | export const decodeToken = (token: string) => {
167 | return jwt.decode(token) as TokenPayload;
168 | };
169 |
170 | export function removeAccents(str: string) {
171 | return str
172 | .normalize("NFD")
173 | .replace(/[\u0300-\u036f]/g, "")
174 | .replace(/đ/g, "d")
175 | .replace(/Đ/g, "D");
176 | }
177 |
178 | export const simpleMatchText = (fullText: string, matchText: string) => {
179 | return removeAccents(fullText.toLowerCase()).includes(
180 | removeAccents(matchText.trim().toLowerCase())
181 | );
182 | };
183 |
184 | export const formatDateTimeToLocaleString = (date: string | Date) => {
185 | return format(
186 | date instanceof Date ? date : new Date(date),
187 | "HH:mm:ss dd/MM/yyyy"
188 | );
189 | };
190 |
191 | export const formatDateTimeToTimeString = (date: string | Date) => {
192 | return format(date instanceof Date ? date : new Date(date), "HH:mm:ss");
193 | };
194 |
195 | export const OrderStatusIcon = {
196 | [OrderStatus.Pending]: Loader,
197 | [OrderStatus.Processing]: CookingPot,
198 | [OrderStatus.Rejected]: BookX,
199 | [OrderStatus.Delivered]: Truck,
200 | [OrderStatus.Paid]: HandCoins,
201 | };
202 |
--------------------------------------------------------------------------------
/src/app/manage/orders/order-guest-detail.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "@/components/ui/badge";
2 | import { Button } from "@/components/ui/button";
3 | import { OrderStatus } from "@/constants/type";
4 | import {
5 | OrderStatusIcon,
6 | formatCurrency,
7 | formatDateTimeToLocaleString,
8 | formatDateTimeToTimeString,
9 | getVietnameseOrderStatus,
10 | handleErrorApi,
11 | } from "@/lib/utils";
12 | import { usePayForGuestMutation } from "@/queries/useOrder";
13 | import {
14 | GetOrdersResType,
15 | PayGuestOrdersResType,
16 | } from "@/schemas/order.schema";
17 | import Image from "next/image";
18 | import { Fragment } from "react";
19 |
20 | type Guest = GetOrdersResType["data"][0]["guest"];
21 | type Orders = GetOrdersResType["data"];
22 | export default function OrderGuestDetail({
23 | guest,
24 | orders,
25 | onPaySuccess,
26 | }: {
27 | guest: Guest;
28 | orders: Orders;
29 | onPaySuccess?: (data: PayGuestOrdersResType) => void;
30 | }) {
31 | const ordersFilterToPurchase = guest
32 | ? orders.filter(
33 | (order) =>
34 | order.status !== OrderStatus.Paid &&
35 | order.status !== OrderStatus.Rejected
36 | )
37 | : [];
38 | const purchasedOrderFilter = guest
39 | ? orders.filter((order) => order.status === OrderStatus.Paid)
40 | : [];
41 | const payForGuestMutation = usePayForGuestMutation();
42 |
43 | const pay = async () => {
44 | if (payForGuestMutation.isPending || !guest) return;
45 | try {
46 | const result = await payForGuestMutation.mutateAsync({
47 | guestId: guest.id,
48 | });
49 | onPaySuccess && onPaySuccess(result.payload);
50 | } catch (error) {
51 | handleErrorApi({
52 | error,
53 | });
54 | }
55 | };
56 | return (
57 |
58 | {guest && (
59 |
60 |
61 | Tên:
62 | {guest.name}
63 | (#{guest.id})
64 | |
65 | Bàn:
66 | {guest.tableNumber}
67 |
68 |
69 | Ngày đăng ký:
70 | {formatDateTimeToLocaleString(guest.createdAt)}
71 |
72 |
73 | )}
74 |
75 |
76 | Đơn hàng:
77 | {orders.map((order, index) => {
78 | return (
79 |
80 | {index + 1}
81 |
82 | {order.status === OrderStatus.Pending && (
83 |
84 | )}
85 | {order.status === OrderStatus.Processing && (
86 |
87 | )}
88 | {order.status === OrderStatus.Rejected && (
89 |
90 | )}
91 | {order.status === OrderStatus.Delivered && (
92 |
93 | )}
94 | {order.status === OrderStatus.Paid && (
95 |
96 | )}
97 |
98 |
106 |
110 | {order.dishSnapshot.name}
111 |
112 |
113 | x{order.quantity}
114 |
115 |
116 | {formatCurrency(order.quantity * order.dishSnapshot.price)}
117 |
118 |
125 | {formatDateTimeToLocaleString(order.createdAt)}
126 |
127 |
134 | {formatDateTimeToTimeString(order.createdAt)}
135 |
136 |
137 | );
138 | })}
139 |
140 |
141 |
142 | Chưa thanh toán:
143 |
144 |
145 | {formatCurrency(
146 | ordersFilterToPurchase.reduce((acc, order) => {
147 | return acc + order.quantity * order.dishSnapshot.price;
148 | }, 0)
149 | )}
150 |
151 |
152 |
153 |
154 | Đã thanh toán:
155 |
156 |
157 | {formatCurrency(
158 | purchasedOrderFilter.reduce((acc, order) => {
159 | return acc + order.quantity * order.dishSnapshot.price;
160 | }, 0)
161 | )}
162 |
163 |
164 |
165 |
166 |
167 |
176 |
177 |
178 | );
179 | }
180 |
--------------------------------------------------------------------------------
|