diff --git a/app/(feed)/feed/page.tsx b/app/(feed)/feed/page.tsx index 7695e90..ab70930 100644 --- a/app/(feed)/feed/page.tsx +++ b/app/(feed)/feed/page.tsx @@ -16,10 +16,10 @@ export default function FeedPage() { {/* Page title */}

- Khám phá quán nước + Explore Shops

- Tìm và chọn quán yêu thích của bạn + Find and choose your favorite shop

@@ -35,7 +35,7 @@ export default function FeedPage() { }} className="cursor-pointer border-none bg-transparent text-sm text-(--color-primary) hover:underline" > - Xóa bộ lọc + Clear filters )} @@ -46,7 +46,7 @@ export default function FeedPage() {
- Lọc quán + Filter shops
{/* Name search */} @@ -54,7 +54,7 @@ export default function FeedPage() { value={searchName} onChange={setSearchName} onClear={() => setSearchName("")} - placeholder="Tìm theo tên quán..." + placeholder="Search by shop name..." className="min-w-0 flex-1" /> @@ -65,13 +65,13 @@ export default function FeedPage() { type="text" value={searchAddress} onChange={(e) => setSearchAddress(e.target.value)} - placeholder="Tìm theo địa chỉ..." + placeholder="Search by address..." className="bg-background text-foreground focus:ring-opacity-20 w-full rounded-xl border border-(--color-border) py-2.5 pr-9 pl-9 text-sm transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)" /> {searchAddress && ( + + +
+ + + + ); +} diff --git a/app/(main)/login/page.tsx b/app/(main)/login/page.tsx index 9883080..5ca3e9d 100644 --- a/app/(main)/login/page.tsx +++ b/app/(main)/login/page.tsx @@ -25,39 +25,12 @@ export default function LoginPage() { {SHOP_INFO.name}

- Đăng nhập vào hệ thống + Enter your phone number to sign in

{/* Login Form */} - - {/* Demo Credentials Info */} -
-

- Tài khoản demo: -

- -
); diff --git a/app/(main)/login/password/page.tsx b/app/(main)/login/password/page.tsx new file mode 100644 index 0000000..d954120 --- /dev/null +++ b/app/(main)/login/password/page.tsx @@ -0,0 +1,176 @@ +"use client"; + +import Button from "@/components/atoms/buttons/Button"; +import ErrorMessageLogin from "@/components/atoms/errors/ErrorMessageLogin"; +import { useAuth } from "@/lib/auth-context"; +import { SHOP_INFO } from "@/lib/constants"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { FormEvent, useEffect, useState } from "react"; + +export default function LoginPasswordPage() { + const router = useRouter(); + const { setUser } = useAuth(); + + const [phone, setPhone] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [errors, setErrors] = useState({ password: "", general: "" }); + + useEffect(() => { + const storedPhone = sessionStorage.getItem("login_phone"); + const storedRole = sessionStorage.getItem("login_role"); + if (!storedPhone || storedRole !== "manager") { + router.replace("/login"); + return; + } + setPhone(storedPhone); + }, [router]); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + + if (!password.trim()) { + setErrors({ password: "Please enter your password", general: "" }); + return; + } + + setIsLoading(true); + setErrors({ password: "", general: "" }); + + try { + const res = await fetch("/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ phone, password, role: "manager" }), + }); + + if (res.ok) { + const userData = await res.json(); + setUser(userData); + localStorage.setItem("coffee-shop-user", JSON.stringify(userData)); + sessionStorage.removeItem("login_phone"); + sessionStorage.removeItem("login_role"); + router.push("/manager"); + } else { + const STATUS_ERROR_MAP: Record = { + 400: "Incorrect login details, please try again", + 401: "Incorrect password", + 403: "Account is locked", + 404: "Account does not exist", + }; + const msg = + STATUS_ERROR_MAP[res.status] ?? + (res.status >= 500 + ? "Server error, please try again later" + : "An error occurred, please try again"); + setErrors({ password: "", general: msg }); + } + } catch { + setErrors({ password: "", general: "Unable to connect, please try again" }); + } finally { + setIsLoading(false); + } + }; + + if (!phone) return null; + + return ( +
+
+ {/* Logo & Shop Name */} +
+
+ {SHOP_INFO.name} +
+

+ {SHOP_INFO.name} +

+

Manager sign in

+
+ + {errors.general && } + +
+ {/* Phone display */} +
+ + {phone} +
+ + {/* Password Input */} +
+ +
+ + { + setPassword(e.target.value); + setErrors({ password: "", general: "" }); + }} + placeholder="Enter password" + disabled={isLoading} + className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-10 py-3 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) disabled:opacity-60 lg:pl-11 ${errors.password ? "border-red-400" : "border-(--color-border)"}`} + /> + +
+ {errors.password && ( + + )} +
+ + {/* Buttons */} +
+ + + +
+
+
+
+ ); +} diff --git a/app/(main)/manager-signup/page.tsx b/app/(main)/manager-signup/page.tsx new file mode 100644 index 0000000..caad475 --- /dev/null +++ b/app/(main)/manager-signup/page.tsx @@ -0,0 +1,196 @@ +"use client"; + +import Button from "@/components/atoms/buttons/Button"; +import { SHOP_INFO } from "@/lib/constants"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { FormEvent, useEffect, useState } from "react"; + +type PageState = "checking" | "available" | "closed" | "error"; + +export default function ManagerSignupPage() { + const router = useRouter(); + + const [pageState, setPageState] = useState("checking"); + const [isLoading, setIsLoading] = useState(false); + const [form, setForm] = useState({ name: "", phone: "", password: "", eateryName: "" }); + const [errors, setErrors] = useState({ name: "", phone: "", password: "", eateryName: "", submit: "" }); + + useEffect(() => { + fetch("/api/manager/signup") + .then((res) => { + setPageState("available") + // if (res.ok) ; + // else if (res.status === 403) setPageState("closed"); + // else setPageState("error"); + }) + .catch(() => setPageState("error")); + }, []); + + const validatePhone = (phone: string) => /^(0[3|5|7|8|9])[0-9]{8}$/.test(phone); + + const handleChange = (field: keyof typeof form) => (e: React.ChangeEvent) => { + setForm({ ...form, [field]: e.target.value }); + setErrors({ ...errors, [field]: "", submit: "" }); + }; + + const validate = () => { + const next = { name: "", phone: "", password: "", eateryName: "", submit: "" }; + if (!form.name.trim()) next.name = "Please enter your full name"; + if (!form.phone.trim()) next.phone = "Please enter your phone number"; + else if (!validatePhone(form.phone)) next.phone = "Invalid phone number (e.g. 0987654321)"; + if (!form.password.trim()) next.password = "Please enter your password"; + else if (form.password.length < 6) next.password = "Password must be at least 6 characters"; + if (!form.eateryName.trim()) next.eateryName = "Please enter the restaurant name"; + setErrors(next); + return !next.name && !next.phone && !next.password && !next.eateryName; + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!validate()) return; + + setIsLoading(true); + try { + const res = await fetch("/api/manager/signup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(form), + }); + + if (res.ok || res.status === 201) { + router.push("/login"); + } else { + const errorCode = (await res.text().catch(() => "")).trim(); + const errorMap: Record = { + ExistedUser: "Phone number already registered", + InvalidPhoneNumber: "Invalid phone number", + }; + setErrors({ ...errors, submit: errorMap[errorCode] ?? "An error occurred, please try again" }); + } + } catch { + setErrors({ ...errors, submit: "Unable to connect, please try again" }); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ {/* Logo */} +
+
+ {SHOP_INFO.name} +
+

{SHOP_INFO.name}

+

Create a manager account

+
+ + {/* Checking */} + {pageState === "checking" && ( +
+ +

Checking...

+
+ )} + + {/* Closed */} + {pageState === "closed" && ( +
+
+ +
+
+

Registration closed

+

The system already has a restaurant. Registration is no longer available.

+
+ + Quay lại đăng nhập + +
+ )} + + {/* Error */} + {pageState === "error" && ( +
+
+ +
+
+

Không thể kết nối

+

Không thể kiểm tra trạng thái đăng ký. Vui lòng thử lại.

+
+ + Quay lại đăng nhập +
+ )} + + {/* Signup Form */} + {pageState === "available" && ( +
+ {[ + { id: "name", label: "Họ tên", icon: "fa-user", placeholder: "Nguyễn Văn A", field: "name" as const, type: "text" }, + { id: "phone", label: "Số điện thoại", icon: "fa-phone", placeholder: "0987654321", field: "phone" as const, type: "tel" }, + { id: "password", label: "Mật khẩu", icon: "fa-lock", placeholder: "Ít nhất 6 ký tự", field: "password" as const, type: "password" }, + { id: "eateryName", label: "Tên nhà hàng", icon: "fa-store", placeholder: "Coffee & More", field: "eateryName" as const, type: "text" }, + ].map(({ id, label, icon, placeholder, field, type }) => ( +
+ +
+ + +
+ {errors[field] && ( +

+ + {errors[field]} +

+ )} +
+ ))} + + {errors.submit && ( +
+ + {errors.submit} +
+ )} + +
+ + + Đã có tài khoản? Đăng nhập + +
+
+ )} +
+
+ ); +} diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx index 7dba76b..295557e 100644 --- a/app/(main)/page.tsx +++ b/app/(main)/page.tsx @@ -38,7 +38,7 @@ export default function Home() { }, [activeCategory]); const activeCategoryLabel = - MENU_CATEGORIES.find((c) => c.id === activeCategory)?.name ?? "Tất cả"; + MENU_CATEGORIES.find((c) => c.id === activeCategory)?.name ?? "All"; return (
@@ -69,7 +69,7 @@ export default function Home() { setSearchQuery(q); }} onClear={() => setSearchQuery("")} - placeholder="Tìm kiếm món..." + placeholder="Search items..." className="sm:max-w-xs" />
diff --git a/app/(main)/payment/page.tsx b/app/(main)/payment/page.tsx index 653c83e..edbfd09 100644 --- a/app/(main)/payment/page.tsx +++ b/app/(main)/payment/page.tsx @@ -46,22 +46,22 @@ export default function PaymentPage() { - Tên sản phẩm + Product name - Giá tiền + Price - Mô tả + Description - Số lượng + Quantity - Xóa + Delete @@ -116,7 +116,7 @@ export default function PaymentPage() { style="payment" aria-label={`Xóa ${item.name} khỏi giỏ hàng`} > - Xóa sản phẩm + Delete product diff --git a/app/(main)/register/page.tsx b/app/(main)/register/page.tsx index 26dba72..6b308c3 100644 --- a/app/(main)/register/page.tsx +++ b/app/(main)/register/page.tsx @@ -6,19 +6,48 @@ import { SHOP_INFO } from "@/lib/constants"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { SubmitEvent, useState } from "react"; +import { FormEvent, useEffect, useState } from "react"; // Static OTP for demo (in production, this would be sent via SMS) const DEMO_OTP = "123456"; export default function RegisterPage() { const router = useRouter(); - const { completeRegistration } = useAuth(); + const { setUser } = useAuth(); const [step, setStep] = useState<"phone" | "otp">("phone"); const [phone, setPhone] = useState(""); const [otp, setOtp] = useState(""); const [errors, setErrors] = useState({ phone: "", otp: "" }); + const [isLoading, setIsLoading] = useState(false); + const [otpSending, setOtpSending] = useState(false); + const [otpSendError, setOtpSendError] = useState(""); + + const sendOtp = async () => { + setOtpSending(true); + setOtpSendError(""); + try { + const res = await fetch("/api/sms_otp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ phone }), + }); + if (!res.ok) { + setOtpSendError("Unable to send OTP, please try again"); + } + } catch { + setOtpSendError("Unable to connect, please try again"); + } finally { + setOtpSending(false); + } + }; + + useEffect(() => { + if (step === "otp") { + sendOtp(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [step]); // Validate Vietnamese phone number const validatePhone = (phoneNumber: string): boolean => { @@ -28,43 +57,86 @@ export default function RegisterPage() { return phoneRegex.test(phoneNumber); }; - const handlePhoneSubmit = (e: SubmitEvent) => { + const handlePhoneSubmit = async (e: FormEvent) => { e.preventDefault(); if (!phone.trim()) { - setErrors({ ...errors, phone: "Vui lòng nhập số điện thoại" }); + setErrors({ ...errors, phone: "Please enter your phone number" }); return; } if (!validatePhone(phone)) { setErrors({ ...errors, - phone: "Số điện thoại không hợp lệ (VD: 0987654321)", + phone: "Invalid phone number (e.g. 0987654321)", }); return; } - // Move to OTP step - setStep("otp"); + setIsLoading(true); setErrors({ phone: "", otp: "" }); + + try { + const res = await fetch("/api/customer/quick_signup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ phone }), + }); + + if (res.ok) { + setStep("otp"); + } else { + const errorCode = (await res.text().catch(() => "")).trim(); + const phoneErrorMap: Record = { + ExistedUser: "Phone number already registered", + InvalidPhoneNumber: "Invalid phone number", + }; + const msg = phoneErrorMap[errorCode] ?? "An error occurred, please try again"; + setErrors({ phone: msg, otp: "" }); + } + } catch { + setErrors({ phone: "Unable to connect, please try again", otp: "" }); + } finally { + setIsLoading(false); + } }; - const handleOtpSubmit = (e: SubmitEvent) => { + const handleOtpSubmit = async (e: FormEvent) => { e.preventDefault(); if (!otp.trim()) { - setErrors({ ...errors, otp: "Vui lòng nhập mã OTP" }); + setErrors({ ...errors, otp: "Please enter your OTP code" }); return; } - if (otp !== DEMO_OTP) { - setErrors({ ...errors, otp: "Mã OTP không đúng" }); - return; - } + setIsLoading(true); + setErrors({ phone: "", otp: "" }); - // Complete registration - completeRegistration(phone); - router.push("/"); + try { + const res = await fetch("/api/customer/quick_login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ phone, otp }), + }); + + if (res.ok) { + const userData = await res.json(); + setUser(userData); + localStorage.setItem("coffee-shop-user", JSON.stringify(userData)); + router.push("/"); + } else { + const errorCode = (await res.text().catch(() => "")).trim(); + const msg = + errorCode === "InvalidOTP" + ? "Incorrect or expired OTP code" + : "An error occurred, please try again"; + setErrors({ phone: "", otp: msg }); + } + } catch { + setErrors({ phone: "", otp: "Unable to connect, please try again" }); + } finally { + setIsLoading(false); + } }; const handleBackToPhone = () => { @@ -94,8 +166,8 @@ export default function RegisterPage() {

{step === "phone" - ? "Đăng ký tài khoản khách hàng" - : "Xác thực số điện thoại"} + ? "Create a customer account" + : "Verify your phone number"}

@@ -131,7 +203,7 @@ export default function RegisterPage() { htmlFor="phone" className="mb-2 block text-sm font-medium text-(--color-text-secondary)" > - Số điện thoại + Phone number
@@ -144,7 +216,8 @@ export default function RegisterPage() { setErrors({ ...errors, phone: "" }); }} placeholder="0987654321" - className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-10 py-3 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) lg:pl-11 ${errors.phone ? "border-red-400" : "border-(--color-border)"} `} + disabled={isLoading} + className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-10 py-3 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) disabled:opacity-60 lg:pl-11 ${errors.phone ? "border-red-400" : "border-(--color-border)"} `} />
{errors.phone && ( @@ -154,7 +227,7 @@ export default function RegisterPage() {

)}

- Nhập số điện thoại Việt Nam (10 số, bắt đầu bằng 0) + Enter a Vietnamese phone number (10 digits, starting with 0)

@@ -166,8 +239,16 @@ export default function RegisterPage() { type="submit" style="login" size="lg" + disabled={isLoading} > - Tiếp tục + {isLoading ? ( + <> + + Processing... + + ) : ( + "Continue" + )} {/* Back to Login */} @@ -175,7 +256,7 @@ export default function RegisterPage() { href="/login" className="flex w-full items-center justify-center rounded-xl border-2 border-(--color-primary) bg-white py-3 font-semibold text-(--color-primary) no-underline transition-all duration-150 hover:bg-(--color-primary) hover:text-white active:scale-98" > - Quay lại đăng nhập + Back to sign in @@ -188,7 +269,16 @@ export default function RegisterPage() {

- Mã OTP đã được gửi đến số {phone} + {otpSending ? ( + <> + + Sending OTP to {phone}... + + ) : ( + <> + OTP code sent to {phone} + + )}

Demo OTP:{" "} @@ -197,6 +287,12 @@ export default function RegisterPage() {

+ {otpSendError && ( +

+ + {otpSendError} +

+ )} {/* OTP Input */}
@@ -204,7 +300,7 @@ export default function RegisterPage() { htmlFor="otp" className="mb-2 block text-sm font-medium text-(--color-text-secondary)" > - Mã OTP + OTP code
@@ -216,9 +312,10 @@ export default function RegisterPage() { setOtp(e.target.value); setErrors({ ...errors, otp: "" }); }} - placeholder="Nhập mã OTP" + placeholder="Enter OTP code" maxLength={6} - className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-4 py-3 text-center text-lg tracking-widest transition-all duration-150 outline-none placeholder:text-sm placeholder:tracking-normal placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) lg:pl-11 ${errors.otp ? "border-red-400" : "border-(--color-border)"} `} + disabled={isLoading} + className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-4 py-3 text-center text-lg tracking-widest transition-all duration-150 outline-none placeholder:text-sm placeholder:tracking-normal placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) disabled:opacity-60 lg:pl-11 ${errors.otp ? "border-red-400" : "border-(--color-border)"} `} />
{errors.otp && ( @@ -237,8 +334,16 @@ export default function RegisterPage() { type="submit" style="login" size="lg" + disabled={isLoading || otpSending} > - Hoàn tất đăng ký + {isLoading ? ( + <> + + Processing... + + ) : ( + "Complete registration" + )} {/* Back Button */} @@ -247,19 +352,28 @@ export default function RegisterPage() { onClick={handleBackToPhone} size="lg" style="login" + disabled={isLoading} > - Thay đổi số điện thoại + Change phone number
- {/* Resend OTP (disabled in demo) */} + {/* Resend OTP */}
diff --git a/app/(manager)/manager/analytics/page.tsx b/app/(manager)/manager/analytics/page.tsx index c0d738e..b38ab8e 100644 --- a/app/(manager)/manager/analytics/page.tsx +++ b/app/(manager)/manager/analytics/page.tsx @@ -28,10 +28,10 @@ import { useMemo, useState } from "react"; // ─── Constants ──────────────────────────────────────────────────────────────── const PERIOD_LABELS: Record = { - day: "Theo ngày", - week: "Theo tuần", - month: "Theo tháng", - year: "Theo năm", + day: "By day", + week: "By week", + month: "By month", + year: "By year", }; const CATEGORY_COLORS = [ @@ -67,7 +67,7 @@ const CHART_META: Record = { function CategorySelect({ value, onChange, - label = "Danh mục:", + label = "Category:", }: { value: string; onChange: (v: string) => void; @@ -78,12 +78,12 @@ function CategorySelect({
setPeriod(e.target.value as AnalyticsPeriod)} className="text-foreground block rounded-lg border border-(--color-border) bg-(--color-bg-card) px-2 py-1.5 text-xs sm:hidden" @@ -226,12 +226,12 @@ export default function AnalyticsPage() { {/* ── Summary Cards ── */}

- Tổng quan + Overview

- Biểu đồ doanh thu + Revenue Chart

{CHART_TYPES.map((t) => ( @@ -298,7 +298,7 @@ export default function AnalyticsPage() { {activeChart === "line" && ( <>

- Doanh thu theo thời gian — {PERIOD_LABELS[period]} + Revenue over time — {PERIOD_LABELS[period]}

@@ -306,7 +306,7 @@ export default function AnalyticsPage() { {activeChart === "bar" && ( <>

- So sánh doanh thu nửa đầu và nửa sau kỳ hiện tại + Comparing revenue of the first and second half of the current period

- Tỷ trọng doanh thu theo danh mục sản phẩm + Revenue share by product category

@@ -330,7 +330,7 @@ export default function AnalyticsPage() {

- Top sản phẩm bán chạy + Top best-selling products

- {p.unitsSold} ly + {p.unitsSold} cups {formatCurrency(p.revenue)} @@ -377,17 +377,17 @@ export default function AnalyticsPage() {

- Phân tích chi tiết sản phẩm + Detailed product analytics

- Click vào tiêu đề cột để sắp xếp. Hiển thị {filteredSales.length}{" "} - sản phẩm. + Click column headers to sort. Showing {filteredSales.length}{" "} + products.

@@ -395,7 +395,7 @@ export default function AnalyticsPage() {
- Tổng doanh thu:{" "} + Total revenue:{" "} {formatCurrencyFull(filteredRevenue)} @@ -403,7 +403,7 @@ export default function AnalyticsPage() {
- Tổng lợi nhuận:{" "} + Total profit:{" "} {formatCurrencyFull(filteredProfit)} @@ -411,15 +411,15 @@ export default function AnalyticsPage() {
- Tổng sản lượng:{" "} + Total units:{" "} - {filteredUnits.toLocaleString()} ly + {filteredUnits.toLocaleString()} cups
- Biên LN trung bình:{" "} + Avg. profit margin:{" "} {avgMargin.toFixed(1)}% diff --git a/app/(manager)/manager/page.tsx b/app/(manager)/manager/page.tsx index 1f6b7f5..78bf7c5 100644 --- a/app/(manager)/manager/page.tsx +++ b/app/(manager)/manager/page.tsx @@ -3,6 +3,7 @@ import { CategoriesTab, CombosTab, + MenuItemsTab, ProductsTab, } from "@/components/organisms/manager"; import { useAuth } from "@/lib/auth-context"; @@ -17,7 +18,7 @@ export default function ManagerPage() { const tabs = [ { id: "products" as const, - label: "Thực đơn", + label: "Menu", icon: "fa-solid fa-utensils", count: products.length, }, @@ -29,10 +30,16 @@ export default function ManagerPage() { }, { id: "categories" as const, - label: "Danh mục", + label: "Categories", icon: "fa-solid fa-tags", count: categories.length, }, + { + id: "menu-items" as const, + label: "Menu", + icon: "fa-solid fa-bowl-food", + count: null, + }, ]; return ( @@ -51,7 +58,7 @@ export default function ManagerPage() {

- {user?.name ?? "Quản lý"} + {user?.name ?? "Manager"}

-

Quản lý quán

+

Store Manager

@@ -121,14 +130,14 @@ export default function ManagerPage() { className="hover:bg-background flex flex-1 items-center justify-center gap-1.5 rounded-xl border border-(--color-border-light) bg-transparent py-2 text-xs font-medium text-(--color-text-secondary) no-underline transition" > - Trang chủ + Home
@@ -139,16 +148,18 @@ export default function ManagerPage() {

- {tabs.find((t) => t.id === activeTab)?.label ?? "Quản lý"} + {tabs.find((t) => t.id === activeTab)?.label ?? "Manager"}

- Quản lý{" "} + Manage{" "} {activeTab === "products" - ? "thực đơn" + ? "menu items" : activeTab === "combos" - ? "combo" - : "danh mục"}{" "} - của quán + ? "combos" + : activeTab === "categories" + ? "categories" + : "menu"}{" "} + for your store

@@ -173,7 +184,7 @@ export default function ManagerPage() { className="flex items-center gap-1.5 rounded-xl border-none bg-(--color-accent-light) px-3 py-2 text-xs font-medium text-(--color-primary) no-underline transition hover:bg-(--color-accent-light)/70" > - Tài chính + Finance
@@ -184,14 +195,14 @@ export default function ManagerPage() { className="flex items-center gap-1.5 rounded-xl bg-(--color-accent-light) px-3 py-2 text-xs font-medium text-(--color-primary) no-underline transition hover:opacity-80" > - Thống kê tài chính + Financial Analytics - Trang chủ + Home
@@ -200,6 +211,7 @@ export default function ManagerPage() { {activeTab === "products" && } {activeTab === "combos" && } {activeTab === "categories" && } + {activeTab === "menu-items" && }
diff --git a/app/(staff)/staff/schedule/page.tsx b/app/(staff)/staff/schedule/page.tsx index 7fa8850..625f8ec 100644 --- a/app/(staff)/staff/schedule/page.tsx +++ b/app/(staff)/staff/schedule/page.tsx @@ -12,18 +12,18 @@ import Link from "next/link"; import { useState } from "react"; const MONTH_NAMES = [ - "Tháng 1", - "Tháng 2", - "Tháng 3", - "Tháng 4", - "Tháng 5", - "Tháng 6", - "Tháng 7", - "Tháng 8", - "Tháng 9", - "Tháng 10", - "Tháng 11", - "Tháng 12", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", ]; function getMonday(d: Date): Date { @@ -95,7 +95,7 @@ export default function StaffSchedulePage() {
-

Lịch làm việc

+

Work Schedule

{isManager ? "Manager" : "Staff"}

@@ -105,7 +105,7 @@ export default function StaffSchedulePage() { {/* View toggle */}
@@ -220,7 +220,7 @@ export default function StaffSchedulePage() {

- Đăng ký ca làm + Register Shift

{view === "week" @@ -241,7 +241,7 @@ export default function StaffSchedulePage() { : "bg-transparent text-(--color-text-secondary)" }`} > - Tuần + Week

{/* Navigation arrows */}
)} {/* Mobile nav */}
- ); -} diff --git a/components/atoms/index.ts b/components/atoms/index.ts index fb05cc0..090fbf4 100644 --- a/components/atoms/index.ts +++ b/components/atoms/index.ts @@ -3,17 +3,13 @@ export { Button } from "./buttons"; export type { ButtonProps } from "./buttons"; // Inputs -export { TextInput, SearchInput, Textarea } from "./inputs"; -export type { TextInputProps, SearchInputProps, TextareaProps } from "./inputs"; +export { TextInput, Textarea } from "./inputs"; +export type { TextInputProps, TextareaProps } from "./inputs"; // Typography export { Heading, Text, Caption } from "./typography"; export type { HeadingProps, TextProps, CaptionProps } from "./typography"; -// Badges -export { Badge, PriceBadge } from "./badges"; -export type { BadgeProps } from "./badges"; - // Dividers export { Divider } from "./dividers"; export type { DividerProps } from "./dividers"; diff --git a/components/atoms/inputs/Input.types.ts b/components/atoms/inputs/Input.types.ts index c062459..0bf9aec 100644 --- a/components/atoms/inputs/Input.types.ts +++ b/components/atoms/inputs/Input.types.ts @@ -13,17 +13,3 @@ export interface TextareaProps extends TextareaHTMLAttributes { - onClear?: () => void; - className?: string; -} - -export interface LoginInputProps extends InputHTMLAttributes { - label: string; - type: string; - name: string; - value: string; - errors?: string; - onChange: (e: React.ChangeEvent) => void; -} diff --git a/components/atoms/inputs/LoginInput.tsx b/components/atoms/inputs/LoginInput.tsx deleted file mode 100644 index 60067df..0000000 --- a/components/atoms/inputs/LoginInput.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useState } from "react"; - -import { LoginInputProps } from "./Input.types"; - -export default function LoginInput({ - label, - type, - name, - value, - errors, - onChange, - ...restProps -}: LoginInputProps) { - const [showPassword, setShowPassword] = useState(false); - - function isPassword() { - if (type === "password") { - return ( - - ); - } - return ""; - } - - return ( -
- -
- - - {isPassword()} -
-
- ); -} diff --git a/components/atoms/inputs/SearchInput.tsx b/components/atoms/inputs/SearchInput.tsx deleted file mode 100644 index 752ced2..0000000 --- a/components/atoms/inputs/SearchInput.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import type { SearchInputProps } from "./Input.types"; - -export default function SearchInput({ - value, - onChange, - onClear, - className = "", - ...props -}: SearchInputProps) { - return ( -
- - - {value && onClear && ( - - )} -
- ); -} diff --git a/components/atoms/inputs/index.ts b/components/atoms/inputs/index.ts index b2b03aa..efdbc24 100644 --- a/components/atoms/inputs/index.ts +++ b/components/atoms/inputs/index.ts @@ -1,9 +1,3 @@ export { default as TextInput } from "./TextInput"; -export { default as SearchInput } from "./SearchInput"; export { default as Textarea } from "./Textarea"; -export type { - TextInputProps, - SearchInputProps, - TextareaProps, - LoginInputProps, -} from "./Input.types"; +export type { TextInputProps, TextareaProps } from "./Input.types"; diff --git a/components/molecules/cards/PaymentSummaryCard.tsx b/components/molecules/cards/PaymentSummaryCard.tsx index adc8505..68923ce 100644 --- a/components/molecules/cards/PaymentSummaryCard.tsx +++ b/components/molecules/cards/PaymentSummaryCard.tsx @@ -23,10 +23,10 @@ export default function PaymentSummaryCard({ return (
diff --git a/components/molecules/cards/ShopCard.tsx b/components/molecules/cards/ShopCard.tsx index cc27ef9..8803239 100644 --- a/components/molecules/cards/ShopCard.tsx +++ b/components/molecules/cards/ShopCard.tsx @@ -28,7 +28,7 @@ export default function ShopCard({ name, address, image }: ShopCardProps) { className="inline-flex shrink-0 items-center gap-1.5 rounded-xl bg-(--color-primary) px-3.5 py-2 text-xs font-semibold text-white no-underline transition-all duration-150 hover:bg-(--color-primary-dark) active:scale-95" > - Xem menu + View menu
diff --git a/components/organisms/forms/LoginForm.tsx b/components/organisms/forms/LoginForm.tsx index 3d9c404..a3cfafe 100644 --- a/components/organisms/forms/LoginForm.tsx +++ b/components/organisms/forms/LoginForm.tsx @@ -1,122 +1,122 @@ +"use client"; + import Button from "@/components/atoms/buttons/Button"; import ErrorMessageLogin from "@/components/atoms/errors/ErrorMessageLogin"; -import LoginInput from "@/components/atoms/inputs/LoginInput"; -import { useAuth } from "@/lib/auth-context"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { SubmitEvent, useState } from "react"; +import { FormEvent, useState } from "react"; + +const PHONE_REGEX = /^(0[35789])[0-9]{8}$/; export default function LoginForm() { const router = useRouter(); - const { login } = useAuth(); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [errors, setErrors] = useState({ - username: "", - password: "", - general: "", - }); + const [phone, setPhone] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [errors, setErrors] = useState({ phone: "", general: "" }); const validate = (): boolean => { - const newErrors = { username: "", password: "", general: "" }; - let isValid = true; - - if (!username.trim()) { - newErrors.username = "Vui lòng nhập tên đăng nhập"; - isValid = false; + if (!phone.trim()) { + setErrors({ phone: "Please enter your phone number", general: "" }); + return false; } - - if (!password.trim()) { - newErrors.password = "Vui lòng nhập mật khẩu"; - isValid = false; - } else if (password.length < 4) { - newErrors.password = "Mật khẩu phải có ít nhất 4 ký tự"; - isValid = false; + if (!PHONE_REGEX.test(phone)) { + setErrors({ phone: "Invalid phone number (e.g. 0987654321)", general: "" }); + return false; } - - setErrors(newErrors); - return isValid; + return true; }; - const handleSubmit = async (e: SubmitEvent) => { + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); - if (!validate()) return; - const success = await login(username, password); + setIsLoading(true); + setErrors({ phone: "", general: "" }); - if (success) { - router.push("/"); - } else { - setErrors({ - username: "", - password: "", - general: "Tên đăng nhập hoặc mật khẩu không đúng", + try { + const res = await fetch("/api/sms_otp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ phone }), }); + + const role = (await res.text().catch(() => "")).trim(); + + if (res.ok && (role === "customer" || role === "manager")) { + sessionStorage.setItem("login_phone", phone); + sessionStorage.setItem("login_role", role); + router.push(role === "manager" ? "/login/password" : "/login/otp"); + } else if (res.status === 404) { + setErrors({ phone: "", general: "Phone number not registered" }); + } else { + setErrors({ phone: "", general: "An error occurred, please try again" }); + } + } catch { + setErrors({ phone: "", general: "Unable to connect, please try again" }); + } finally { + setIsLoading(false); } }; return (
- {/* Error Message */} {errors.general && }
- {/* Username Input */} - { - setUsername(e.target.value); - setErrors({ ...errors, username: "", general: "" }); - }} - errors={errors.username} - /> - {errors.username && ( - + +
+ + { + setPhone(e.target.value); + setErrors({ phone: "", general: "" }); + }} + placeholder="0987654321" + disabled={isLoading} + className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-10 py-3 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) disabled:opacity-60 lg:pl-11 ${errors.phone ? "border-red-400" : "border-(--color-border)"}`} + /> +
+ {errors.phone && ( + )} +

+ Enter a Vietnamese phone number (10 digits, starting with 0) +

- {/* Password Input */} -
- { - setPassword(e.target.value); - setErrors({ ...errors, password: "", general: "" }); - }} - errors={errors.password} - /> - {errors.password && ( - - )} -
- - {/* Buttons */}
- {/* Login Button */} - {/* Register Button */} - Đăng ký tài khoản + Create account
diff --git a/components/organisms/manager/CategoriesTab.tsx b/components/organisms/manager/CategoriesTab.tsx index dcc03b4..598499d 100644 --- a/components/organisms/manager/CategoriesTab.tsx +++ b/components/organisms/manager/CategoriesTab.tsx @@ -23,15 +23,15 @@ export default function CategoriesTab() {

- {categories.length} danh - mục + {categories.length}{" "} + categories

@@ -50,19 +50,19 @@ export default function CategoriesTab() {

{cat.name}

-

{count} món

+

{count} items

diff --git a/components/organisms/manager/CategoryModal.tsx b/components/organisms/manager/CategoryModal.tsx index b93e808..92c618c 100644 --- a/components/organisms/manager/CategoryModal.tsx +++ b/components/organisms/manager/CategoryModal.tsx @@ -52,7 +52,7 @@ export default function CategoryModal({

- {isEdit ? "Chỉnh sửa danh mục" : "Thêm danh mục mới"} + {isEdit ? "Edit category" : "Add new category"}

+
+ + {/* Table */} +
+ + + + + + + + + {menuItems.length === 0 ? ( + + + + ) : ( + menuItems.map((item) => ( + + + + + )) + )} + +
+ Tên món + + Giá +
+ + Chưa có món nào trong menu +
+

{item.name}

+
+ {formatPrice(item.price)} +
+
+ + {/* Add Item Modal */} + {showForm && ( +
+
+
+

Thêm món mới

+ +
+ +
+
+ + setNewName(e.target.value)} + placeholder="Nhập tên món..." + required + className="text-foreground w-full rounded-xl border border-(--color-border) bg-white px-3 py-2 text-sm outline-none transition focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)/20" + /> +
+ +
+ + setNewPrice(e.target.value)} + placeholder="Nhập giá..." + required + min={0} + step={500} + className="text-foreground w-full rounded-xl border border-(--color-border) bg-white px-3 py-2 text-sm outline-none transition focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)/20" + /> +
+ + {submitError && ( +

+ + {submitError} +

+ )} + +
+ + +
+
+
+
+ )} +
+ ); +} diff --git a/components/organisms/manager/StatusBadge.tsx b/components/organisms/manager/StatusBadge.tsx index 4abffb0..56ddd0f 100644 --- a/components/organisms/manager/StatusBadge.tsx +++ b/components/organisms/manager/StatusBadge.tsx @@ -14,7 +14,7 @@ export default function StatusBadge({ available }: StatusBadgeProps) { available ? "bg-emerald-500" : "bg-amber-500" }`} /> - {available ? "Còn hàng" : "Tạm hết"} + {available ? "In stock" : "Out of stock"} ); } diff --git a/components/organisms/manager/index.ts b/components/organisms/manager/index.ts index c756d71..bdc1d5a 100644 --- a/components/organisms/manager/index.ts +++ b/components/organisms/manager/index.ts @@ -6,6 +6,7 @@ export { default as ComboModal } from "./ComboModal"; export { default as ProductsTab } from "./ProductsTab"; export { default as CategoriesTab } from "./CategoriesTab"; export { default as CombosTab } from "./CombosTab"; +export { default as MenuItemsTab } from "./MenuItemsTab"; export type { ProductModalProps, CategoryModalProps, diff --git a/components/organisms/modals/ReviewModal.tsx b/components/organisms/modals/ReviewModal.tsx index a1e8c0a..0ba7ce3 100644 --- a/components/organisms/modals/ReviewModal.tsx +++ b/components/organisms/modals/ReviewModal.tsx @@ -49,13 +49,13 @@ export default function ReviewModal({ isOpen, onClose }: ReviewModalProps) {
- Cảm ơn quý khách + Thank you - Chúng tôi trân trọng đánh giá của bạn! + We appreciate your feedback! ) : ( @@ -65,21 +65,21 @@ export default function ReviewModal({ isOpen, onClose }: ReviewModalProps) { id="review-modal-title" className="text-foreground mb-1 text-xl font-bold" > - Đánh giá của bạn + Your Review

- Hãy cho chúng tôi biết trải nghiệm của bạn hôm nay + Tell us about your experience today

{/* Star rating */}

- Mức độ hài lòng + Satisfaction level

{[1, 2, 3, 4, 5].map((star) => { const isActive = star <= (hovered || rating); @@ -90,7 +90,7 @@ export default function ReviewModal({ isOpen, onClose }: ReviewModalProps) { onClick={() => setRating(star)} onMouseEnter={() => setHovered(star)} onMouseLeave={() => setHovered(0)} - aria-label={`${star} sao`} + aria-label={`${star} star`} aria-pressed={rating === star} className="text-3xl transition-transform hover:scale-110 active:scale-95 sm:text-4xl" > @@ -108,7 +108,7 @@ export default function ReviewModal({ isOpen, onClose }: ReviewModalProps) { {rating > 0 && (

{ - ["", "Rất tệ", "Tệ", "Bình thường", "Tốt", "Xuất sắc"][ + ["", "Very poor", "Poor", "Average", "Good", "Excellent"][ rating ] } @@ -120,10 +120,10 @@ export default function ReviewModal({ isOpen, onClose }: ReviewModalProps) {