This commit is contained in:
@@ -16,10 +16,10 @@ export default function FeedPage() {
|
||||
{/* Page title */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-foreground text-2xl font-bold md:text-3xl">
|
||||
Khám phá quán nước
|
||||
Explore Shops
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-(--color-text-muted)">
|
||||
Tìm và chọn quán yêu thích của bạn
|
||||
Find and choose your favorite shop
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -46,7 +46,7 @@ export default function FeedPage() {
|
||||
<div className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center">
|
||||
<div className="flex shrink-0 items-center gap-2 text-sm font-semibold text-(--color-text-secondary)">
|
||||
<i className="fa-solid fa-filter text-(--color-primary)"></i>
|
||||
<span>Lọc quán</span>
|
||||
<span>Filter shops</span>
|
||||
</div>
|
||||
|
||||
{/* 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 && (
|
||||
<button
|
||||
onClick={() => setSearchAddress("")}
|
||||
aria-label="Xóa tìm kiếm địa chỉ"
|
||||
aria-label="Clear address search"
|
||||
className="absolute top-1/2 right-3 -translate-y-1/2 cursor-pointer border-none bg-transparent p-0 text-(--color-text-muted) transition-colors duration-150 hover:text-(--color-primary)"
|
||||
>
|
||||
<i className="fa-solid fa-xmark text-sm"></i>
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function LoginOtpPage() {
|
||||
e.preventDefault();
|
||||
|
||||
if (!otp.trim()) {
|
||||
setErrors({ otp: "Vui lòng nhập mã OTP", general: "" });
|
||||
setErrors({ otp: "Please enter your OTP code", general: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,12 +63,12 @@ export default function LoginOtpPage() {
|
||||
const errorCode = (await res.text().catch(() => "")).trim();
|
||||
const msg =
|
||||
errorCode === "InvalidOTP"
|
||||
? "Mã OTP không đúng hoặc đã hết hạn"
|
||||
: "Đã có lỗi xảy ra, vui lòng thử lại";
|
||||
? "Incorrect or expired OTP code"
|
||||
: "An error occurred, please try again";
|
||||
setErrors({ otp: msg, general: "" });
|
||||
}
|
||||
} catch {
|
||||
setErrors({ otp: "", general: "Không thể kết nối, vui lòng thử lại" });
|
||||
setErrors({ otp: "", general: "Unable to connect, please try again" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export default function LoginOtpPage() {
|
||||
{SHOP_INFO.name}
|
||||
</h1>
|
||||
<p className="text-sm text-(--color-text-muted)">
|
||||
Xác thực số điện thoại
|
||||
Verify your phone number
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function LoginOtpPage() {
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||
<p className="text-sm text-blue-800">
|
||||
<i className="fa-solid fa-circle-info mr-2"></i>
|
||||
Mã OTP đã được gửi đến số <strong>{phone}</strong>
|
||||
OTP code sent to <strong>{phone}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -116,7 +116,7 @@ export default function LoginOtpPage() {
|
||||
htmlFor="otp"
|
||||
className="mb-2 block text-sm font-medium text-(--color-text-secondary)"
|
||||
>
|
||||
Mã OTP
|
||||
OTP code
|
||||
</label>
|
||||
<div className="relative">
|
||||
<i className="fa-solid fa-key absolute top-1/2 left-4 hidden -translate-y-1/2 text-(--color-text-muted) lg:block"></i>
|
||||
@@ -128,7 +128,7 @@ export default function LoginOtpPage() {
|
||||
setOtp(e.target.value);
|
||||
setErrors({ otp: "", general: "" });
|
||||
}}
|
||||
placeholder="Nhập mã OTP"
|
||||
placeholder="Enter OTP code"
|
||||
maxLength={6}
|
||||
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)"}`}
|
||||
@@ -151,10 +151,10 @@ export default function LoginOtpPage() {
|
||||
{isLoading ? (
|
||||
<>
|
||||
<i className="fa-solid fa-spinner fa-spin mr-2"></i>
|
||||
Đang xử lý...
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
"Đăng nhập"
|
||||
"Sign in"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -163,7 +163,7 @@ export default function LoginOtpPage() {
|
||||
onClick={() => router.push("/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) transition-all duration-150 hover:bg-(--color-primary) hover:text-white active:scale-98"
|
||||
>
|
||||
Thay đổi số điện thoại
|
||||
Change phone number
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function LoginPage() {
|
||||
{SHOP_INFO.name}
|
||||
</h1>
|
||||
<p className="text-sm text-(--color-text-muted)">
|
||||
Nhập số điện thoại để đăng nhập
|
||||
Enter your phone number to sign in
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function LoginPasswordPage() {
|
||||
e.preventDefault();
|
||||
|
||||
if (!password.trim()) {
|
||||
setErrors({ password: "Vui lòng nhập mật khẩu", general: "" });
|
||||
setErrors({ password: "Please enter your password", general: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,20 +55,20 @@ export default function LoginPasswordPage() {
|
||||
router.push("/manager");
|
||||
} else {
|
||||
const STATUS_ERROR_MAP: Record<number, string> = {
|
||||
400: "Sai thông tin đăng nhập, vui lòng thử lại",
|
||||
401: "Mật khẩu không đúng",
|
||||
403: "Tài khoản bị khóa",
|
||||
404: "Tài khoản không tồn tại",
|
||||
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
|
||||
? "Lỗi máy chủ, vui lòng thử lại sau"
|
||||
: "Đã có lỗi xảy ra, vui lòng thử lại");
|
||||
? "Server error, please try again later"
|
||||
: "An error occurred, please try again");
|
||||
setErrors({ password: "", general: msg });
|
||||
}
|
||||
} catch {
|
||||
setErrors({ password: "", general: "Không thể kết nối, vui lòng thử lại" });
|
||||
setErrors({ password: "", general: "Unable to connect, please try again" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -94,7 +94,7 @@ export default function LoginPasswordPage() {
|
||||
<h1 className="mb-1 text-2xl font-bold text-(--color-primary-dark)">
|
||||
{SHOP_INFO.name}
|
||||
</h1>
|
||||
<p className="text-sm text-(--color-text-muted)">Đăng nhập quản lý</p>
|
||||
<p className="text-sm text-(--color-text-muted)">Manager sign in</p>
|
||||
</div>
|
||||
|
||||
{errors.general && <ErrorMessageLogin message={errors.general} />}
|
||||
@@ -112,7 +112,7 @@ export default function LoginPasswordPage() {
|
||||
htmlFor="password"
|
||||
className="mb-2 block text-sm font-medium text-(--color-text-secondary)"
|
||||
>
|
||||
Mật khẩu
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<i className="fa-solid fa-lock absolute top-1/2 left-4 hidden -translate-y-1/2 text-(--color-text-muted) lg:block"></i>
|
||||
@@ -124,7 +124,7 @@ export default function LoginPasswordPage() {
|
||||
setPassword(e.target.value);
|
||||
setErrors({ password: "", general: "" });
|
||||
}}
|
||||
placeholder="Nhập mật khẩu"
|
||||
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)"}`}
|
||||
/>
|
||||
@@ -132,7 +132,7 @@ export default function LoginPasswordPage() {
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute top-1/2 right-4 -translate-y-1/2 text-(--color-text-muted) transition-colors hover:text-(--color-primary)"
|
||||
aria-label={showPassword ? "Ẩn mật khẩu" : "Hiện mật khẩu"}
|
||||
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||
>
|
||||
<i className={`fa-solid ${showPassword ? "fa-eye-slash" : "fa-eye"}`}></i>
|
||||
</button>
|
||||
@@ -154,10 +154,10 @@ export default function LoginPasswordPage() {
|
||||
{isLoading ? (
|
||||
<>
|
||||
<i className="fa-solid fa-spinner fa-spin mr-2"></i>
|
||||
Đang xử lý...
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
"Đăng nhập"
|
||||
"Sign in"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -166,7 +166,7 @@ export default function LoginPasswordPage() {
|
||||
onClick={() => router.push("/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) transition-all duration-150 hover:bg-(--color-primary) hover:text-white active:scale-98"
|
||||
>
|
||||
Thay đổi số điện thoại
|
||||
Change phone number
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -37,12 +37,12 @@ export default function ManagerSignupPage() {
|
||||
|
||||
const validate = () => {
|
||||
const next = { name: "", phone: "", password: "", eateryName: "", submit: "" };
|
||||
if (!form.name.trim()) next.name = "Vui lòng nhập họ tên";
|
||||
if (!form.phone.trim()) next.phone = "Vui lòng nhập số điện thoại";
|
||||
else if (!validatePhone(form.phone)) next.phone = "Số điện thoại không hợp lệ (VD: 0987654321)";
|
||||
if (!form.password.trim()) next.password = "Vui lòng nhập mật khẩu";
|
||||
else if (form.password.length < 6) next.password = "Mật khẩu ít nhất 6 ký tự";
|
||||
if (!form.eateryName.trim()) next.eateryName = "Vui lòng nhập tên nhà hàng";
|
||||
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;
|
||||
};
|
||||
@@ -64,13 +64,13 @@ export default function ManagerSignupPage() {
|
||||
} else {
|
||||
const errorCode = (await res.text().catch(() => "")).trim();
|
||||
const errorMap: Record<string, string> = {
|
||||
ExistedUser: "Số điện thoại đã được đăng ký",
|
||||
InvalidPhoneNumber: "Số điện thoại không hợp lệ",
|
||||
ExistedUser: "Phone number already registered",
|
||||
InvalidPhoneNumber: "Invalid phone number",
|
||||
};
|
||||
setErrors({ ...errors, submit: errorMap[errorCode] ?? "Đã có lỗi xảy ra, vui lòng thử lại" });
|
||||
setErrors({ ...errors, submit: errorMap[errorCode] ?? "An error occurred, please try again" });
|
||||
}
|
||||
} catch {
|
||||
setErrors({ ...errors, submit: "Không thể kết nối, vui lòng thử lại" });
|
||||
setErrors({ ...errors, submit: "Unable to connect, please try again" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -85,14 +85,14 @@ export default function ManagerSignupPage() {
|
||||
<Image src={SHOP_INFO.logo} alt={SHOP_INFO.name} fill className="object-contain" sizes="80px" priority />
|
||||
</div>
|
||||
<h1 className="mb-1 text-2xl font-bold text-(--color-primary-dark)">{SHOP_INFO.name}</h1>
|
||||
<p className="text-sm text-(--color-text-muted)">Đăng ký tài khoản quản lý</p>
|
||||
<p className="text-sm text-(--color-text-muted)">Create a manager account</p>
|
||||
</div>
|
||||
|
||||
{/* Checking */}
|
||||
{pageState === "checking" && (
|
||||
<div className="flex flex-col items-center gap-3 py-8 text-(--color-text-muted)">
|
||||
<i className="fa-solid fa-spinner fa-spin text-2xl text-(--color-primary)"></i>
|
||||
<p className="text-sm">Đang kiểm tra...</p>
|
||||
<p className="text-sm">Checking...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -103,8 +103,8 @@ export default function ManagerSignupPage() {
|
||||
<i className="fa-solid fa-lock text-2xl text-red-500"></i>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h2 className="mb-2 text-lg font-semibold text-(--color-text-primary)">Đăng ký đã đóng</h2>
|
||||
<p className="text-sm text-(--color-text-muted)">Hệ thống đã có nhà hàng. Chức năng đăng ký không còn khả dụng.</p>
|
||||
<h2 className="mb-2 text-lg font-semibold text-(--color-text-primary)">Registration closed</h2>
|
||||
<p className="text-sm text-(--color-text-muted)">The system already has a restaurant. Registration is no longer available.</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/login"
|
||||
|
||||
+2
-2
@@ -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 (
|
||||
<div className="bg-background flex min-h-[calc(100vh-var(--spacing-header-height))] items-start">
|
||||
@@ -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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -46,22 +46,22 @@ export default function PaymentPage() {
|
||||
<thead>
|
||||
<tr className="bg-(--color-border-light)/40 text-left">
|
||||
<th scope="col" className="px-4 py-3 font-semibold">
|
||||
Tên sản phẩm
|
||||
Product name
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 font-semibold">
|
||||
Giá tiền
|
||||
Price
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 font-semibold">
|
||||
Mô tả
|
||||
Description
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 font-semibold">
|
||||
Số lượng
|
||||
Quantity
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-4 py-3 text-right font-semibold"
|
||||
>
|
||||
Xóa
|
||||
Delete
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -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
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -33,10 +33,10 @@ export default function RegisterPage() {
|
||||
body: JSON.stringify({ phone }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setOtpSendError("Không thể gửi OTP, vui lòng thử lại");
|
||||
setOtpSendError("Unable to send OTP, please try again");
|
||||
}
|
||||
} catch {
|
||||
setOtpSendError("Không thể kết nối, vui lòng thử lại");
|
||||
setOtpSendError("Unable to connect, please try again");
|
||||
} finally {
|
||||
setOtpSending(false);
|
||||
}
|
||||
@@ -61,14 +61,14 @@ export default function RegisterPage() {
|
||||
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;
|
||||
}
|
||||
@@ -88,14 +88,14 @@ export default function RegisterPage() {
|
||||
} else {
|
||||
const errorCode = (await res.text().catch(() => "")).trim();
|
||||
const phoneErrorMap: Record<string, string> = {
|
||||
ExistedUser: "Số điện thoại đã được đăng ký",
|
||||
InvalidPhoneNumber: "Số điện thoại không hợp lệ",
|
||||
ExistedUser: "Phone number already registered",
|
||||
InvalidPhoneNumber: "Invalid phone number",
|
||||
};
|
||||
const msg = phoneErrorMap[errorCode] ?? "Đã có lỗi xảy ra, vui lòng thử lại";
|
||||
const msg = phoneErrorMap[errorCode] ?? "An error occurred, please try again";
|
||||
setErrors({ phone: msg, otp: "" });
|
||||
}
|
||||
} catch {
|
||||
setErrors({ phone: "Không thể kết nối, vui lòng thử lại", otp: "" });
|
||||
setErrors({ phone: "Unable to connect, please try again", otp: "" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export default function RegisterPage() {
|
||||
e.preventDefault();
|
||||
|
||||
if (!otp.trim()) {
|
||||
setErrors({ ...errors, otp: "Vui lòng nhập mã OTP" });
|
||||
setErrors({ ...errors, otp: "Please enter your OTP code" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -128,12 +128,12 @@ export default function RegisterPage() {
|
||||
const errorCode = (await res.text().catch(() => "")).trim();
|
||||
const msg =
|
||||
errorCode === "InvalidOTP"
|
||||
? "Mã OTP không đúng hoặc đã hết hạn"
|
||||
: "Đã có lỗi xảy ra, vui lòng thử lại";
|
||||
? "Incorrect or expired OTP code"
|
||||
: "An error occurred, please try again";
|
||||
setErrors({ phone: "", otp: msg });
|
||||
}
|
||||
} catch {
|
||||
setErrors({ phone: "", otp: "Không thể kết nối, vui lòng thử lại" });
|
||||
setErrors({ phone: "", otp: "Unable to connect, please try again" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -166,8 +166,8 @@ export default function RegisterPage() {
|
||||
</h1>
|
||||
<p className="text-sm text-(--color-text-muted)">
|
||||
{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"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -203,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
|
||||
</label>
|
||||
<div className="relative">
|
||||
<i className="fa-solid fa-phone absolute top-1/2 left-4 hidden -translate-y-1/2 text-(--color-text-muted) lg:block"></i>
|
||||
@@ -227,7 +227,7 @@ export default function RegisterPage() {
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-2 text-xs text-(--color-text-muted)">
|
||||
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)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -244,10 +244,10 @@ export default function RegisterPage() {
|
||||
{isLoading ? (
|
||||
<>
|
||||
<i className="fa-solid fa-spinner fa-spin mr-2"></i>
|
||||
Đang xử lý...
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
"Tiếp tục"
|
||||
"Continue"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -256,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
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
@@ -272,11 +272,11 @@ export default function RegisterPage() {
|
||||
{otpSending ? (
|
||||
<>
|
||||
<i className="fa-solid fa-spinner fa-spin mr-1"></i>
|
||||
Đang gửi OTP đến <strong>{phone}</strong>...
|
||||
Sending OTP to <strong>{phone}</strong>...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Mã OTP đã được gửi đến số <strong>{phone}</strong>
|
||||
OTP code sent to <strong>{phone}</strong>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
@@ -300,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
|
||||
</label>
|
||||
<div className="relative">
|
||||
<i className="fa-solid fa-key absolute top-1/2 left-4 hidden -translate-y-1/2 text-(--color-text-muted) lg:block"></i>
|
||||
@@ -312,7 +312,7 @@ export default function RegisterPage() {
|
||||
setOtp(e.target.value);
|
||||
setErrors({ ...errors, otp: "" });
|
||||
}}
|
||||
placeholder="Nhập mã OTP"
|
||||
placeholder="Enter OTP code"
|
||||
maxLength={6}
|
||||
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)"} `}
|
||||
@@ -339,10 +339,10 @@ export default function RegisterPage() {
|
||||
{isLoading ? (
|
||||
<>
|
||||
<i className="fa-solid fa-spinner fa-spin mr-2"></i>
|
||||
Đang xử lý...
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
"Hoàn tất đăng ký"
|
||||
"Complete registration"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -354,7 +354,7 @@ export default function RegisterPage() {
|
||||
style="login"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Thay đổi số điện thoại
|
||||
Change phone number
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -369,10 +369,10 @@ export default function RegisterPage() {
|
||||
{otpSending ? (
|
||||
<>
|
||||
<i className="fa-solid fa-spinner fa-spin mr-1"></i>
|
||||
Đang gửi lại...
|
||||
Resending...
|
||||
</>
|
||||
) : (
|
||||
"Gửi lại mã OTP"
|
||||
"Resend OTP code"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -28,10 +28,10 @@ import { useMemo, useState } from "react";
|
||||
// ─── Constants ────────────────────────────────────────────────────────────────
|
||||
|
||||
const PERIOD_LABELS: Record<AnalyticsPeriod, string> = {
|
||||
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<ChartType, { icon: string; label: string }> = {
|
||||
function CategorySelect({
|
||||
value,
|
||||
onChange,
|
||||
label = "Danh mục:",
|
||||
label = "Category:",
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
@@ -78,12 +78,12 @@ function CategorySelect({
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs text-(--color-text-muted)">{label}</label>
|
||||
<select
|
||||
title="Danh mục"
|
||||
title="Category"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="bg-background text-foreground rounded-lg border border-(--color-border) px-2 py-1.5 text-xs"
|
||||
>
|
||||
<option value="all">Tất cả</option>
|
||||
<option value="all">All</option>
|
||||
{categories.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
@@ -181,7 +181,7 @@ export default function AnalyticsPage() {
|
||||
</span>
|
||||
<div>
|
||||
<h1 className="text-foreground text-lg leading-tight font-bold">
|
||||
Thống kê & Phân tích tài chính
|
||||
Financial Statistics & Analytics
|
||||
</h1>
|
||||
<p className="text-xs text-(--color-text-muted)">
|
||||
Financial Analytics Dashboard
|
||||
@@ -205,7 +205,7 @@ export default function AnalyticsPage() {
|
||||
</button>
|
||||
))}
|
||||
<select
|
||||
title="Chọn kỳ"
|
||||
title="Select period"
|
||||
value={period}
|
||||
onChange={(e) => 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 ── */}
|
||||
<section>
|
||||
<h2 className="mb-3 text-sm font-semibold tracking-wider text-(--color-text-muted) uppercase">
|
||||
Tổng quan
|
||||
Overview
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<SummaryCard
|
||||
icon="fa-solid fa-sack-dollar"
|
||||
title="Tổng doanh thu"
|
||||
title="Total Revenue"
|
||||
value={formatCurrency(totalRevenue)}
|
||||
subtitle={PERIOD_LABELS[period]}
|
||||
change={revComp.change}
|
||||
@@ -240,27 +240,27 @@ export default function AnalyticsPage() {
|
||||
/>
|
||||
<SummaryCard
|
||||
icon="fa-solid fa-receipt"
|
||||
title="Số đơn hàng"
|
||||
title="Total Orders"
|
||||
value={totalOrders.toLocaleString()}
|
||||
subtitle="Tổng đơn trong kỳ"
|
||||
subtitle="Total orders in period"
|
||||
change={ordComp.change}
|
||||
changePercent={ordComp.changePercent}
|
||||
isPositive={ordComp.isPositive}
|
||||
/>
|
||||
<SummaryCard
|
||||
icon="fa-solid fa-circle-dollar-to-slot"
|
||||
title="Tổng lợi nhuận"
|
||||
title="Total Profit"
|
||||
value={formatCurrency(totalProfit)}
|
||||
subtitle="Ước tính từ dữ liệu bán hàng"
|
||||
subtitle="Estimated from sales data"
|
||||
change={proComp.change}
|
||||
changePercent={proComp.changePercent}
|
||||
isPositive={proComp.isPositive}
|
||||
/>
|
||||
<SummaryCard
|
||||
icon="fa-solid fa-basket-shopping"
|
||||
title="Giá trị đơn TB"
|
||||
title="Avg. Order Value"
|
||||
value={formatCurrency(avgOrderValue)}
|
||||
subtitle="Doanh thu / số đơn hàng"
|
||||
subtitle="Revenue / number of orders"
|
||||
change={0}
|
||||
changePercent={0}
|
||||
isPositive={true}
|
||||
@@ -273,7 +273,7 @@ export default function AnalyticsPage() {
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<h2 className="text-foreground text-base font-semibold">
|
||||
<i className="fa-solid fa-chart-area mr-2 text-(--color-primary)"></i>
|
||||
Biểu đồ doanh thu
|
||||
Revenue Chart
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
{CHART_TYPES.map((t) => (
|
||||
@@ -298,7 +298,7 @@ export default function AnalyticsPage() {
|
||||
{activeChart === "line" && (
|
||||
<>
|
||||
<p className="mb-3 text-xs text-(--color-text-muted)">
|
||||
Doanh thu theo thời gian — {PERIOD_LABELS[period]}
|
||||
Revenue over time — {PERIOD_LABELS[period]}
|
||||
</p>
|
||||
<LineChart data={revenueData} height={220} />
|
||||
</>
|
||||
@@ -306,7 +306,7 @@ export default function AnalyticsPage() {
|
||||
{activeChart === "bar" && (
|
||||
<>
|
||||
<p className="mb-3 text-xs text-(--color-text-muted)">
|
||||
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
|
||||
</p>
|
||||
<BarChart
|
||||
current={barCurrent}
|
||||
@@ -318,7 +318,7 @@ export default function AnalyticsPage() {
|
||||
{activeChart === "pie" && (
|
||||
<>
|
||||
<p className="mb-3 text-xs text-(--color-text-muted)">
|
||||
Tỷ trọng doanh thu theo danh mục sản phẩm
|
||||
Revenue share by product category
|
||||
</p>
|
||||
<PieChart data={pieData} />
|
||||
</>
|
||||
@@ -330,7 +330,7 @@ export default function AnalyticsPage() {
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<h2 className="text-foreground text-base font-semibold">
|
||||
<i className="fa-solid fa-fire mr-2 text-orange-500"></i>
|
||||
Top sản phẩm bán chạy
|
||||
Top best-selling products
|
||||
</h2>
|
||||
<CategorySelect
|
||||
value={categoryFilter}
|
||||
@@ -353,7 +353,7 @@ export default function AnalyticsPage() {
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-3 text-xs">
|
||||
<span className="text-(--color-text-muted) tabular-nums">
|
||||
{p.unitsSold} ly
|
||||
{p.unitsSold} cups
|
||||
</span>
|
||||
<span className="font-semibold text-(--color-primary) tabular-nums">
|
||||
{formatCurrency(p.revenue)}
|
||||
@@ -377,17 +377,17 @@ export default function AnalyticsPage() {
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<h2 className="text-foreground text-base font-semibold">
|
||||
<i className="fa-solid fa-table text-foreground mr-2"></i>
|
||||
Phân tích chi tiết sản phẩm
|
||||
Detailed product analytics
|
||||
</h2>
|
||||
<CategorySelect
|
||||
value={categoryFilter}
|
||||
onChange={setCategoryFilter}
|
||||
label="Lọc danh mục:"
|
||||
label="Filter category:"
|
||||
/>
|
||||
</div>
|
||||
<p className="mb-3 text-xs text-(--color-text-muted)">
|
||||
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.
|
||||
</p>
|
||||
<ProductTable data={filteredSales} />
|
||||
|
||||
@@ -395,7 +395,7 @@ export default function AnalyticsPage() {
|
||||
<div className="bg-background mt-4 flex flex-wrap gap-4 rounded-xl p-4 text-sm">
|
||||
<div>
|
||||
<span className="text-(--color-text-muted)">
|
||||
Tổng doanh thu:{" "}
|
||||
Total revenue:{" "}
|
||||
</span>
|
||||
<span className="font-semibold text-(--color-primary)">
|
||||
{formatCurrencyFull(filteredRevenue)}
|
||||
@@ -403,7 +403,7 @@ export default function AnalyticsPage() {
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-(--color-text-muted)">
|
||||
Tổng lợi nhuận:{" "}
|
||||
Total profit:{" "}
|
||||
</span>
|
||||
<span className="font-semibold text-green-600">
|
||||
{formatCurrencyFull(filteredProfit)}
|
||||
@@ -411,15 +411,15 @@ export default function AnalyticsPage() {
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-(--color-text-muted)">
|
||||
Tổng sản lượng:{" "}
|
||||
Total units:{" "}
|
||||
</span>
|
||||
<span className="text-foreground font-semibold">
|
||||
{filteredUnits.toLocaleString()} ly
|
||||
{filteredUnits.toLocaleString()} cups
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-(--color-text-muted)">
|
||||
Biên LN trung bình:{" "}
|
||||
Avg. profit margin:{" "}
|
||||
</span>
|
||||
<span className="font-semibold text-yellow-700">
|
||||
{avgMargin.toFixed(1)}%
|
||||
|
||||
@@ -18,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,
|
||||
},
|
||||
@@ -30,7 +30,7 @@ export default function ManagerPage() {
|
||||
},
|
||||
{
|
||||
id: "categories" as const,
|
||||
label: "Danh mục",
|
||||
label: "Categories",
|
||||
icon: "fa-solid fa-tags",
|
||||
count: categories.length,
|
||||
},
|
||||
@@ -58,7 +58,7 @@ export default function ManagerPage() {
|
||||
|
||||
<nav className="flex-1 space-y-1 p-3">
|
||||
<p className="mb-2 px-3 text-[11px] font-semibold tracking-wider text-(--color-text-muted) uppercase">
|
||||
Quản lý thực đơn
|
||||
Menu Management
|
||||
</p>
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
@@ -87,16 +87,16 @@ export default function ManagerPage() {
|
||||
))}
|
||||
<div className="mt-3 border-t border-(--color-border-light) pt-3">
|
||||
<p className="mb-2 px-3 text-[11px] font-semibold tracking-wider text-(--color-text-muted) uppercase">
|
||||
Phân tích
|
||||
Analytics
|
||||
</p>
|
||||
<Link
|
||||
href="/manager/analytics"
|
||||
className="hover:bg-background flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-(--color-text-secondary) no-underline transition-all hover:text-(--color-primary-dark)"
|
||||
>
|
||||
<i className="fa-solid fa-chart-line w-4 text-center"></i>
|
||||
<span className="flex-1 text-left">Tài chính</span>
|
||||
<span className="flex-1 text-left">Finance</span>
|
||||
<span className="rounded-full bg-(--color-accent-light) px-2 py-0.5 text-xs font-semibold text-(--color-primary)">
|
||||
Mới
|
||||
New
|
||||
</span>
|
||||
</Link>
|
||||
<Link
|
||||
@@ -104,9 +104,9 @@ export default function ManagerPage() {
|
||||
className="hover:bg-background flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-(--color-text-secondary) no-underline transition-all hover:text-(--color-primary-dark)"
|
||||
>
|
||||
<i className="fa-solid fa-calendar-days w-4 text-center"></i>
|
||||
<span className="flex-1 text-left">Ca làm</span>
|
||||
<span className="flex-1 text-left">Shifts</span>
|
||||
<span className="rounded-full bg-(--color-accent-light) px-2 py-0.5 text-xs font-semibold text-(--color-primary)">
|
||||
Mới
|
||||
New
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -119,9 +119,9 @@ export default function ManagerPage() {
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-foreground truncate text-sm font-semibold">
|
||||
{user?.name ?? "Quản lý"}
|
||||
{user?.name ?? "Manager"}
|
||||
</p>
|
||||
<p className="text-xs text-(--color-text-muted)">Quản lý quán</p>
|
||||
<p className="text-xs text-(--color-text-muted)">Store Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 flex gap-2 px-1">
|
||||
@@ -130,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"
|
||||
>
|
||||
<i className="fa-solid fa-house"></i>
|
||||
Trang chủ
|
||||
Home
|
||||
</Link>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="flex flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-xl border-none bg-transparent py-2 text-xs font-medium text-red-500 transition hover:bg-red-50"
|
||||
>
|
||||
<i className="fa-solid fa-right-from-bracket"></i>
|
||||
Đăng xuất
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,18 +148,18 @@ export default function ManagerPage() {
|
||||
<header className="sticky top-0 z-40 flex items-center justify-between border-b border-(--color-border-light) bg-white px-5 py-4 shadow-sm">
|
||||
<div>
|
||||
<h1 className="text-foreground text-lg font-bold">
|
||||
{tabs.find((t) => t.id === activeTab)?.label ?? "Quản lý"}
|
||||
{tabs.find((t) => t.id === activeTab)?.label ?? "Manager"}
|
||||
</h1>
|
||||
<p className="text-xs text-(--color-text-muted)">
|
||||
Quản lý{" "}
|
||||
Manage{" "}
|
||||
{activeTab === "products"
|
||||
? "thực đơn"
|
||||
? "menu items"
|
||||
: activeTab === "combos"
|
||||
? "combo"
|
||||
? "combos"
|
||||
: activeTab === "categories"
|
||||
? "danh mục"
|
||||
? "categories"
|
||||
: "menu"}{" "}
|
||||
của quán
|
||||
for your store
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -184,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"
|
||||
>
|
||||
<i className="fa-solid fa-chart-line"></i>
|
||||
<span className="hidden sm:inline">Tài chính</span>
|
||||
<span className="hidden sm:inline">Finance</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -195,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"
|
||||
>
|
||||
<i className="fa-solid fa-chart-line"></i>
|
||||
Thống kê tài chính
|
||||
Financial Analytics
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="hover:bg-background flex items-center gap-1.5 rounded-xl border border-(--color-border-light) bg-transparent px-3 py-2 text-xs font-medium text-(--color-text-secondary) no-underline transition"
|
||||
>
|
||||
<i className="fa-solid fa-house"></i>
|
||||
Trang chủ
|
||||
Home
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -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() {
|
||||
<i className="fa-solid fa-calendar-days text-sm text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-foreground text-sm font-bold">Lịch làm việc</p>
|
||||
<p className="text-foreground text-sm font-bold">Work Schedule</p>
|
||||
<p className="text-xs text-(--color-text-muted)">
|
||||
{isManager ? "Manager" : "Staff"}
|
||||
</p>
|
||||
@@ -105,7 +105,7 @@ export default function StaffSchedulePage() {
|
||||
{/* View toggle */}
|
||||
<nav className="flex-1 space-y-1 p-3">
|
||||
<p className="mb-2 px-3 text-[11px] font-semibold tracking-wider text-(--color-text-muted) uppercase">
|
||||
Chế độ xem
|
||||
View
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
@@ -117,7 +117,7 @@ export default function StaffSchedulePage() {
|
||||
}`}
|
||||
>
|
||||
<i className="fa-solid fa-table-columns w-4 text-center"></i>
|
||||
<span className="flex-1 text-left">Theo tuần</span>
|
||||
<span className="flex-1 text-left">Weekly</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -129,13 +129,13 @@ export default function StaffSchedulePage() {
|
||||
}`}
|
||||
>
|
||||
<i className="fa-solid fa-calendar w-4 text-center"></i>
|
||||
<span className="flex-1 text-left">Theo tháng</span>
|
||||
<span className="flex-1 text-left">Monthly</span>
|
||||
</button>
|
||||
|
||||
{/* Quick nav */}
|
||||
<div className="mt-3 border-t border-(--color-border-light) pt-3">
|
||||
<p className="mb-2 px-3 text-[11px] font-semibold tracking-wider text-(--color-text-muted) uppercase">
|
||||
Điều hướng
|
||||
Navigation
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
@@ -143,7 +143,7 @@ export default function StaffSchedulePage() {
|
||||
className="hover:bg-background flex w-full cursor-pointer items-center gap-3 rounded-xl border-none bg-transparent px-3 py-2.5 text-sm font-medium text-(--color-text-secondary) transition-all hover:text-(--color-primary-dark)"
|
||||
>
|
||||
<i className="fa-solid fa-crosshairs w-4 text-center"></i>
|
||||
<span className="flex-1 text-left">Hôm nay</span>
|
||||
<span className="flex-1 text-left">Today</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -151,7 +151,7 @@ export default function StaffSchedulePage() {
|
||||
{isManager && (
|
||||
<div className="mt-3 border-t border-(--color-border-light) pt-3">
|
||||
<p className="mb-2 px-3 text-[11px] font-semibold tracking-wider text-(--color-text-muted) uppercase">
|
||||
Quản lý
|
||||
Management
|
||||
</p>
|
||||
<Link
|
||||
href="/manager"
|
||||
@@ -167,7 +167,7 @@ export default function StaffSchedulePage() {
|
||||
<div className="mt-3 border-t border-(--color-border-light) pt-3">
|
||||
<div className="rounded-xl bg-(--color-primary)/5 p-3">
|
||||
<p className="text-[10px] font-semibold text-(--color-text-muted) uppercase">
|
||||
Ngân sách tuần
|
||||
Weekly Budget
|
||||
</p>
|
||||
<p className="mt-1 text-lg font-bold text-(--color-primary)">
|
||||
{weeklyBudget.toLocaleString("vi-VN")}
|
||||
@@ -187,10 +187,10 @@ export default function StaffSchedulePage() {
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-foreground truncate text-sm font-semibold">
|
||||
{user?.name ?? "Nhân viên"}
|
||||
{user?.name ?? "Staff"}
|
||||
</p>
|
||||
<p className="text-xs text-(--color-text-muted)">
|
||||
{isManager ? "Quản lý" : "Nhân viên"}
|
||||
{isManager ? "Manager" : "Staff"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,7 +200,7 @@ export default function StaffSchedulePage() {
|
||||
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"
|
||||
>
|
||||
<i className="fa-solid fa-house"></i>
|
||||
Trang chủ
|
||||
Home
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
@@ -208,7 +208,7 @@ export default function StaffSchedulePage() {
|
||||
className="flex flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-xl border-none bg-transparent py-2 text-xs font-medium text-red-500 transition hover:bg-red-50"
|
||||
>
|
||||
<i className="fa-solid fa-right-from-bracket"></i>
|
||||
Đăng xuất
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,7 +220,7 @@ export default function StaffSchedulePage() {
|
||||
<header className="sticky top-0 z-40 flex items-center justify-between border-b border-(--color-border-light) bg-white px-4 py-3 shadow-sm md:px-5 md:py-4">
|
||||
<div>
|
||||
<h1 className="text-foreground text-base font-bold md:text-lg">
|
||||
Đăng ký ca làm
|
||||
Register Shift
|
||||
</h1>
|
||||
<p className="text-xs text-(--color-text-muted)">
|
||||
{view === "week"
|
||||
@@ -241,7 +241,7 @@ export default function StaffSchedulePage() {
|
||||
: "bg-transparent text-(--color-text-secondary)"
|
||||
}`}
|
||||
>
|
||||
Tuần
|
||||
Week
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -252,14 +252,14 @@ export default function StaffSchedulePage() {
|
||||
: "bg-transparent text-(--color-text-secondary)"
|
||||
}`}
|
||||
>
|
||||
Tháng
|
||||
Month
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation arrows */}
|
||||
<div className="hidden items-center gap-1 md:flex">
|
||||
<button
|
||||
title="Về trước"
|
||||
title="Previous"
|
||||
type="button"
|
||||
onClick={view === "week" ? goToPrevWeek : goToPrevMonth}
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-(--color-border-light) bg-transparent text-(--color-text-muted) transition hover:bg-gray-50"
|
||||
@@ -271,10 +271,10 @@ export default function StaffSchedulePage() {
|
||||
onClick={goToToday}
|
||||
className="cursor-pointer rounded-lg border border-(--color-border-light) bg-transparent px-3 py-1.5 text-xs font-medium text-(--color-text-secondary) transition hover:bg-gray-50"
|
||||
>
|
||||
Hôm nay
|
||||
Today
|
||||
</button>
|
||||
<button
|
||||
title="Tiếp theo"
|
||||
title="Next"
|
||||
type="button"
|
||||
onClick={view === "week" ? goToNextWeek : goToNextMonth}
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-(--color-border-light) bg-transparent text-(--color-text-muted) transition hover:bg-gray-50"
|
||||
@@ -294,14 +294,14 @@ export default function StaffSchedulePage() {
|
||||
className="hidden cursor-pointer items-center gap-1.5 rounded-xl border-none bg-(--color-primary) px-3 py-2 text-xs font-semibold text-white transition hover:opacity-90 md:flex"
|
||||
>
|
||||
<i className="fa-solid fa-plus"></i>
|
||||
Tạo ca
|
||||
Create Shift
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Mobile nav */}
|
||||
<div className="flex items-center gap-1 md:hidden">
|
||||
<button
|
||||
title="Về trước"
|
||||
title="Previous"
|
||||
type="button"
|
||||
onClick={view === "week" ? goToPrevWeek : goToPrevMonth}
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-(--color-text-muted)"
|
||||
@@ -309,7 +309,7 @@ export default function StaffSchedulePage() {
|
||||
<i className="fa-solid fa-chevron-left text-xs"></i>
|
||||
</button>
|
||||
<button
|
||||
title="Tiếp theo"
|
||||
title="Next"
|
||||
type="button"
|
||||
onClick={view === "week" ? goToNextWeek : goToNextMonth}
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-(--color-text-muted)"
|
||||
@@ -353,7 +353,7 @@ export default function StaffSchedulePage() {
|
||||
{/* Mobile FAB for manager */}
|
||||
{isManager && (
|
||||
<button
|
||||
title="Tạo ca"
|
||||
title="Create Shift"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCreateDate(undefined);
|
||||
|
||||
@@ -23,10 +23,10 @@ export default function PaymentSummaryCard({
|
||||
return (
|
||||
<aside className="shrink-0 xl:w-85">
|
||||
<div className="bg-card sticky top-[calc(var(--spacing-header-height)+1rem)] rounded-2xl border border-(--color-border-light) p-4 md:p-5">
|
||||
<h2 className="mb-4 text-lg font-bold">Hóa đơn</h2>
|
||||
<h2 className="mb-4 text-lg font-bold">Bill</h2>
|
||||
|
||||
<div className="flex items-center justify-between border-b border-(--color-border-light) pb-4">
|
||||
<span className="text-(--color-text-muted)">Tổng cộng</span>
|
||||
<span className="text-(--color-text-muted)">Total</span>
|
||||
<span className="text-xl font-bold text-(--color-primary)">
|
||||
{formatPrice(totalPrice)}
|
||||
</span>
|
||||
@@ -40,7 +40,7 @@ export default function PaymentSummaryCard({
|
||||
size="md"
|
||||
variant="primary"
|
||||
>
|
||||
Tiền mặt
|
||||
Cash
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -61,7 +61,7 @@ export default function PaymentSummaryCard({
|
||||
size="md"
|
||||
variant="primary"
|
||||
>
|
||||
Đánh giá
|
||||
Review
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function PaymentSummaryCard({
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
>
|
||||
Quay về
|
||||
Return
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function ProductCard({
|
||||
icon="fa-cart-plus"
|
||||
aria-label={`Mua ${productName}`}
|
||||
>
|
||||
Mua
|
||||
Buy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<i className="fa-solid fa-book-open text-[10px]"></i>
|
||||
Xem menu
|
||||
View menu
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ export default function LoginForm() {
|
||||
|
||||
const validate = (): boolean => {
|
||||
if (!phone.trim()) {
|
||||
setErrors({ phone: "Vui lòng nhập số điện thoại", general: "" });
|
||||
setErrors({ phone: "Please enter your phone number", general: "" });
|
||||
return false;
|
||||
}
|
||||
if (!PHONE_REGEX.test(phone)) {
|
||||
setErrors({ phone: "Số điện thoại không hợp lệ (VD: 0987654321)", general: "" });
|
||||
setErrors({ phone: "Invalid phone number (e.g. 0987654321)", general: "" });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -48,12 +48,12 @@ export default function LoginForm() {
|
||||
sessionStorage.setItem("login_role", role);
|
||||
router.push(role === "manager" ? "/login/password" : "/login/otp");
|
||||
} else if (res.status === 404) {
|
||||
setErrors({ phone: "", general: "Số điện thoại chưa đăng ký" });
|
||||
setErrors({ phone: "", general: "Phone number not registered" });
|
||||
} else {
|
||||
setErrors({ phone: "", general: "Đã có lỗi xảy ra, vui lòng thử lại" });
|
||||
setErrors({ phone: "", general: "An error occurred, please try again" });
|
||||
}
|
||||
} catch {
|
||||
setErrors({ phone: "", general: "Không thể kết nối, vui lòng thử lại" });
|
||||
setErrors({ phone: "", general: "Unable to connect, please try again" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export default function LoginForm() {
|
||||
htmlFor="phone"
|
||||
className="mb-2 block text-sm font-medium text-(--color-text-secondary)"
|
||||
>
|
||||
Số điện thoại
|
||||
Phone number
|
||||
</label>
|
||||
<div className="relative">
|
||||
<i className="fa-solid fa-phone absolute top-1/2 left-4 hidden -translate-y-1/2 text-(--color-text-muted) lg:block"></i>
|
||||
@@ -90,7 +90,7 @@ export default function LoginForm() {
|
||||
<ErrorMessageLogin message={errors.phone} type="secondary" />
|
||||
)}
|
||||
<p className="mt-2 text-xs text-(--color-text-muted)">
|
||||
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)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -105,10 +105,10 @@ export default function LoginForm() {
|
||||
{isLoading ? (
|
||||
<>
|
||||
<i className="fa-solid fa-spinner fa-spin mr-2"></i>
|
||||
Đang xử lý...
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
"Tiếp tục"
|
||||
"Continue"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -116,7 +116,7 @@ export default function LoginForm() {
|
||||
href="/register"
|
||||
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"
|
||||
>
|
||||
Đăng ký tài khoản
|
||||
Create account
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -23,15 +23,15 @@ export default function CategoriesTab() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-(--color-text-muted)">
|
||||
<strong className="text-foreground">{categories.length}</strong> danh
|
||||
mục
|
||||
<strong className="text-foreground">{categories.length}</strong>{" "}
|
||||
categories
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setModalCategory("new")}
|
||||
className="flex cursor-pointer items-center gap-2 rounded-xl border-none bg-(--color-primary) px-4 py-2 text-sm font-semibold text-white transition hover:bg-(--color-primary-dark) active:scale-95"
|
||||
>
|
||||
<i className="fa-solid fa-plus"></i>
|
||||
<span className="hidden sm:inline">Thêm danh mục</span>
|
||||
<span className="hidden sm:inline">Add category</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -50,19 +50,19 @@ export default function CategoriesTab() {
|
||||
<p className="text-foreground truncate font-semibold">
|
||||
{cat.name}
|
||||
</p>
|
||||
<p className="text-xs text-(--color-text-muted)">{count} món</p>
|
||||
<p className="text-xs text-(--color-text-muted)">{count} items</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<button
|
||||
onClick={() => setModalCategory(cat)}
|
||||
title="Chỉnh sửa"
|
||||
title="Edit"
|
||||
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-lg border border-(--color-border-light) bg-transparent text-(--color-text-muted) transition hover:border-(--color-primary-light) hover:text-(--color-primary)"
|
||||
>
|
||||
<i className="fa-solid fa-pen text-[11px]"></i>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleteTarget(cat)}
|
||||
title="Xóa"
|
||||
title="Delete"
|
||||
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-transparent text-(--color-text-muted) transition hover:border-red-200 hover:bg-red-50 hover:text-red-500"
|
||||
>
|
||||
<i className="fa-solid fa-trash text-[11px]"></i>
|
||||
@@ -75,7 +75,7 @@ export default function CategoriesTab() {
|
||||
{categories.length === 0 && (
|
||||
<div className="col-span-full flex flex-col items-center gap-3 py-16 text-(--color-text-muted)">
|
||||
<i className="fa-solid fa-tag text-4xl opacity-30"></i>
|
||||
<p className="text-sm">Chưa có danh mục nào</p>
|
||||
<p className="text-sm">No categories yet</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function CategoryModal({
|
||||
<div className="w-full max-w-md rounded-2xl bg-white shadow-2xl">
|
||||
<div className="flex items-center justify-between border-b border-(--color-border-light) px-6 py-4">
|
||||
<h2 className="text-foreground text-lg font-bold">
|
||||
{isEdit ? "Chỉnh sửa danh mục" : "Thêm danh mục mới"}
|
||||
{isEdit ? "Edit category" : "Add new category"}
|
||||
</h2>
|
||||
<button
|
||||
title="Close"
|
||||
@@ -66,7 +66,7 @@ export default function CategoryModal({
|
||||
<form onSubmit={handleSubmit} className="space-y-4 px-6 py-5">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-(--color-text-secondary)">
|
||||
Tên danh mục <span className="text-red-500">*</span>
|
||||
Category name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
@@ -74,7 +74,7 @@ export default function CategoryModal({
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
className="text-foreground w-full rounded-xl border border-(--color-border) bg-white px-3 py-2 text-sm transition outline-none focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)/20"
|
||||
placeholder="Ví dụ: Cà Phê"
|
||||
placeholder="e.g. Coffee"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
import { gqlFetch } from "@/lib/graphql";
|
||||
import { addManagerMenuItem, gqlFetch } from "@/lib/graphql";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
interface MenuItem {
|
||||
@@ -76,13 +76,8 @@ export default function MenuItemsTab() {
|
||||
setSubmitting(true);
|
||||
setSubmitError(null);
|
||||
try {
|
||||
const data = await gqlFetch<{ addMenuItem: MenuItem }>(
|
||||
`mutation($name: String!, $price: Float!) {
|
||||
addMenuItem(name: $name, price: $price) { id name price }
|
||||
}`,
|
||||
{ name: newName.trim(), price: parseFloat(newPrice) },
|
||||
);
|
||||
setMenuItems((prev) => [...prev, data.addMenuItem]);
|
||||
const added = await addManagerMenuItem(newName.trim(), parseFloat(newPrice));
|
||||
if (added) setMenuItems((prev) => [...prev, added]);
|
||||
setNewName("");
|
||||
setNewPrice("");
|
||||
setShowForm(false);
|
||||
|
||||
@@ -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"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,13 +49,13 @@ export default function ReviewModal({ isOpen, onClose }: ReviewModalProps) {
|
||||
<i className="fa-solid fa-heart text-(--color-accent)"></i>
|
||||
</div>
|
||||
<Heading level={2} id="review-modal-title">
|
||||
Cảm ơn quý khách
|
||||
Thank you
|
||||
</Heading>
|
||||
<Text variant="body2" className="mt-2">
|
||||
Chúng tôi trân trọng đánh giá của bạn!
|
||||
We appreciate your feedback!
|
||||
</Text>
|
||||
<Button onClick={handleClose} variant="primary" className="mt-4">
|
||||
Đóng
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -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
|
||||
</h2>
|
||||
<p className="mb-5 text-sm text-(--color-text-muted)">
|
||||
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
|
||||
</p>
|
||||
|
||||
{/* Star rating */}
|
||||
<div className="mb-5">
|
||||
<p className="mb-2 text-sm font-medium text-(--color-text-secondary)">
|
||||
Mức độ hài lòng
|
||||
Satisfaction level
|
||||
</p>
|
||||
<div
|
||||
className="flex gap-2"
|
||||
role="radiogroup"
|
||||
aria-label="Xếp hạng sao"
|
||||
aria-label="Star rating"
|
||||
>
|
||||
{[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 && (
|
||||
<p className="mt-1.5 text-xs text-(--color-text-muted)">
|
||||
{
|
||||
["", "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) {
|
||||
<div className="mb-6">
|
||||
<Textarea
|
||||
id="review-text"
|
||||
label="Nhận xét (tùy chọn)"
|
||||
label="Comment (optional)"
|
||||
value={review}
|
||||
onChange={(e) => setReview(e.target.value)}
|
||||
placeholder="Chia sẻ cảm nhận của bạn về đồ uống, dịch vụ..."
|
||||
placeholder="Share your thoughts on the drinks, service..."
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
@@ -137,7 +137,7 @@ export default function ReviewModal({ isOpen, onClose }: ReviewModalProps) {
|
||||
className="flex-1"
|
||||
icon="fa-arrow-left"
|
||||
>
|
||||
Quay lại
|
||||
Go back
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -146,7 +146,7 @@ export default function ReviewModal({ isOpen, onClose }: ReviewModalProps) {
|
||||
className="flex-1"
|
||||
icon="fa-check"
|
||||
>
|
||||
Xác nhận
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useMemo, useState } from "react";
|
||||
|
||||
import type { MobileShiftViewProps } from "./ShiftSchedule.types";
|
||||
|
||||
const DAY_HEADERS = ["T2", "T3", "T4", "T5", "T6", "T7", "CN"];
|
||||
const DAY_HEADERS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
|
||||
function formatDateISO(d: Date): string {
|
||||
const y = d.getFullYear();
|
||||
@@ -26,18 +26,18 @@ function isToday(d: Date): boolean {
|
||||
}
|
||||
|
||||
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",
|
||||
];
|
||||
|
||||
export default function MobileShiftView({
|
||||
@@ -94,7 +94,7 @@ export default function MobileShiftView({
|
||||
{/* Month navigation */}
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<button
|
||||
title="Trở lại tháng trước"
|
||||
title="Previous month"
|
||||
type="button"
|
||||
onClick={goToPrevMonth}
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border-none bg-transparent text-(--color-text-muted) transition hover:bg-gray-100"
|
||||
@@ -105,7 +105,7 @@ export default function MobileShiftView({
|
||||
{MONTH_NAMES[currentDate.getMonth()]} {currentDate.getFullYear()}
|
||||
</h3>
|
||||
<button
|
||||
title="Trở lại tháng sau"
|
||||
title="Next month"
|
||||
type="button"
|
||||
onClick={goToNextMonth}
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border-none bg-transparent text-(--color-text-muted) transition hover:bg-gray-100"
|
||||
@@ -173,22 +173,22 @@ export default function MobileShiftView({
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-amber-400"></span>
|
||||
<span className="text-[10px] text-(--color-text-muted)">
|
||||
Còn trống
|
||||
Available
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-green-500"></span>
|
||||
<span className="text-[10px] text-(--color-text-muted)">Đã ĐK</span>
|
||||
<span className="text-[10px] text-(--color-text-muted)">Registered</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-purple-400"></span>
|
||||
<span className="text-[10px] text-(--color-text-muted)">
|
||||
Nghỉ phép
|
||||
On leave
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-red-400"></span>
|
||||
<span className="text-[10px] text-(--color-text-muted)">Vắng</span>
|
||||
<span className="text-[10px] text-(--color-text-muted)">Absent</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,7 +199,7 @@ export default function MobileShiftView({
|
||||
{dayOfWeek}, {selectedDateObj.getDate()}/
|
||||
{selectedDateObj.getMonth() + 1}/{selectedDateObj.getFullYear()}
|
||||
<span className="ml-2 text-xs font-normal text-(--color-text-muted)">
|
||||
({selectedShifts.length} ca)
|
||||
({selectedShifts.length} shift{selectedShifts.length !== 1 ? "s" : ""})
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
@@ -207,7 +207,7 @@ export default function MobileShiftView({
|
||||
<div className="rounded-xl border border-dashed border-(--color-border-light) py-8 text-center">
|
||||
<i className="fa-regular fa-calendar-xmark mb-2 text-2xl text-gray-300"></i>
|
||||
<p className="text-sm text-(--color-text-muted)">
|
||||
Không có ca làm trong ngày này
|
||||
No shifts for this day
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useMemo } from "react";
|
||||
|
||||
import type { MonthlyCalendarProps } from "./ShiftSchedule.types";
|
||||
|
||||
const DAY_HEADERS = ["T2", "T3", "T4", "T5", "T6", "T7", "CN"];
|
||||
const DAY_HEADERS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
|
||||
function formatDateISO(d: Date): string {
|
||||
const y = d.getFullYear();
|
||||
@@ -125,7 +125,7 @@ export default function MonthlyCalendar({
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="h-2 w-2 rounded-full bg-blue-400"></span>
|
||||
<span className="text-[10px] text-blue-600">
|
||||
{summary.available} trống
|
||||
{summary.available} available
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -133,7 +133,7 @@ export default function MonthlyCalendar({
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="h-2 w-2 rounded-full bg-blue-700"></span>
|
||||
<span className="text-[10px] text-blue-800">
|
||||
{summary.registered} đã ĐK
|
||||
{summary.registered} registered
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -141,7 +141,7 @@ export default function MonthlyCalendar({
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="h-2 w-2 rounded-full bg-purple-400"></span>
|
||||
<span className="text-[10px] text-purple-600">
|
||||
{summary.leave} nghỉ
|
||||
{summary.leave} on leave
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -149,7 +149,7 @@ export default function MonthlyCalendar({
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="h-2 w-2 rounded-full bg-red-400"></span>
|
||||
<span className="text-[10px] text-red-600">
|
||||
{summary.absent} vắng
|
||||
{summary.absent} absent
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function ShiftCreateModal({
|
||||
|
||||
// Validate
|
||||
if (!date || !startTime || !endTime) {
|
||||
setError("Vui lòng điền đầy đủ thông tin.");
|
||||
setError("Please fill in all required fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function ShiftCreateModal({
|
||||
const endMinutes = eh * 60 + em;
|
||||
|
||||
if (endMinutes <= startMinutes) {
|
||||
setError("Giờ kết thúc phải sau giờ bắt đầu.");
|
||||
setError("End time must be after start time.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,10 +77,10 @@ export default function ShiftCreateModal({
|
||||
<div className="flex items-center justify-between border-b border-(--color-border-light) px-5 py-4">
|
||||
<div>
|
||||
<h2 className="text-foreground text-base font-bold">
|
||||
Tạo ca làm mới
|
||||
Create New Shift
|
||||
</h2>
|
||||
<p className="text-xs text-(--color-text-muted)">
|
||||
Thêm khung giờ ca làm cho nhân viên
|
||||
Add a shift time slot for staff
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -98,7 +98,7 @@ export default function ShiftCreateModal({
|
||||
{/* Date */}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-semibold text-(--color-text-secondary)">
|
||||
Ngày
|
||||
Date
|
||||
</label>
|
||||
<input
|
||||
title="Date"
|
||||
@@ -113,7 +113,7 @@ export default function ShiftCreateModal({
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-semibold text-(--color-text-secondary)">
|
||||
Giờ bắt đầu
|
||||
Start Time
|
||||
</label>
|
||||
<input
|
||||
title="Start Time"
|
||||
@@ -125,7 +125,7 @@ export default function ShiftCreateModal({
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-semibold text-(--color-text-secondary)">
|
||||
Giờ kết thúc
|
||||
End Time
|
||||
</label>
|
||||
<input
|
||||
title="End Time"
|
||||
|
||||
@@ -22,7 +22,7 @@ const MONTH_NAMES_EN = [
|
||||
"December",
|
||||
];
|
||||
|
||||
const DAY_LABELS = ["T2", "T3", "T4", "T5", "T6", "T7", "CN"];
|
||||
const DAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
const DAY_LABELS_EN = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
|
||||
function formatDateShort(d: Date): string {
|
||||
@@ -94,7 +94,7 @@ export default function WeeklySchedule({
|
||||
<div className="rounded-xl border border-(--color-border-light) bg-white p-3">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<button
|
||||
title="Tuần trước"
|
||||
title="Previous week"
|
||||
type="button"
|
||||
onClick={goToPrevWeek}
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border-none bg-transparent text-(--color-text-muted) transition hover:bg-gray-100"
|
||||
@@ -105,7 +105,7 @@ export default function WeeklySchedule({
|
||||
{MONTH_NAMES_EN[currentDate.getMonth()]} {currentDate.getFullYear()}
|
||||
</h3>
|
||||
<button
|
||||
title="Tuần sau"
|
||||
title="Next week"
|
||||
type="button"
|
||||
onClick={goToNextWeek}
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border-none bg-transparent text-(--color-text-muted) transition hover:bg-gray-100"
|
||||
@@ -192,7 +192,7 @@ export default function WeeklySchedule({
|
||||
className="flex w-full cursor-pointer items-center justify-center rounded-lg border border-dashed border-gray-300 bg-transparent py-2 text-xs text-gray-400 transition hover:border-(--color-primary) hover:text-(--color-primary)"
|
||||
>
|
||||
<i className="fa-solid fa-plus mr-1"></i>
|
||||
Thêm ca
|
||||
Add shift
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -213,7 +213,7 @@ export default function WeeklySchedule({
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-28 border-r border-b border-(--color-border-light) bg-gray-50 px-3 py-3 text-left text-xs font-semibold text-(--color-text-muted) uppercase">
|
||||
Bộ phận
|
||||
Department
|
||||
</th>
|
||||
{weekDates.map((date, i) => (
|
||||
<th
|
||||
@@ -273,7 +273,7 @@ export default function WeeklySchedule({
|
||||
className="mt-auto flex cursor-pointer items-center justify-center rounded-lg border border-dashed border-gray-300 bg-transparent py-1 text-[10px] text-gray-400 transition hover:border-(--color-primary) hover:text-(--color-primary)"
|
||||
>
|
||||
<i className="fa-solid fa-plus mr-1"></i>
|
||||
Thêm ca
|
||||
Add shift
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function ShopGrid({
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 py-24 text-(--color-text-muted)">
|
||||
<i className="fa-solid fa-store text-5xl opacity-30"></i>
|
||||
<p className="text-base font-medium">Không tìm thấy quán nào phù hợp</p>
|
||||
<p className="text-base font-medium">No shops found matching your search</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+9
-9
@@ -48,7 +48,7 @@ export default function Footer() {
|
||||
<ul className="flex flex-col gap-2 text-sm opacity-80">
|
||||
<li className="flex items-start gap-2">
|
||||
<i className="fa-solid fa-location-dot mt-0.5 w-4 shrink-0 text-center text-(--color-accent)"></i>
|
||||
<span>Địa chỉ: {SHOP_INFO.address}</span>
|
||||
<span>Address: {SHOP_INFO.address}</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<i className="fa-solid fa-phone w-4 shrink-0 text-center text-(--color-accent)"></i>
|
||||
@@ -56,7 +56,7 @@ export default function Footer() {
|
||||
href={`tel:${SHOP_INFO.phone}`}
|
||||
className="transition-colors duration-150 hover:text-(--color-accent)"
|
||||
>
|
||||
Số điện thoại: {SHOP_INFO.phone}
|
||||
Phone: {SHOP_INFO.phone}
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
@@ -83,7 +83,7 @@ export default function Footer() {
|
||||
{/* ── 2. Social links ── */}
|
||||
<div className="col-span-1">
|
||||
<h3 className="mb-4 text-sm font-bold tracking-wider text-(--color-accent) uppercase">
|
||||
Kết nối
|
||||
Follow Us
|
||||
</h3>
|
||||
<ul className="flex flex-col gap-3">
|
||||
<li>
|
||||
@@ -129,20 +129,20 @@ export default function Footer() {
|
||||
{/* ── 3. WiFi card ── */}
|
||||
<div className="col-span-1">
|
||||
<h3 className="mb-4 text-sm font-bold tracking-wider text-(--color-accent) uppercase">
|
||||
WiFi Miễn Phí
|
||||
Free WiFi
|
||||
</h3>
|
||||
<div className="border-opacity-50 bg-opacity-30 rounded-xl border border-(--color-primary-light) bg-(--color-primary-dark) p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<i className="fa-solid fa-wifi shrink-0 text-lg text-(--color-accent)"></i>
|
||||
<span className="text-sm font-semibold">
|
||||
Kết nối miễn phí
|
||||
Connect for free
|
||||
</span>
|
||||
</div>
|
||||
{/* Stacked label + value rows — no overflow risk */}
|
||||
<div className="flex flex-col gap-3 text-sm">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs tracking-wide uppercase opacity-60">
|
||||
Tên mạng
|
||||
Network name
|
||||
</span>
|
||||
<span className="border-opacity-30 rounded border border-(--color-accent) px-2 py-1 font-mono font-bold break-all text-(--color-accent)">
|
||||
{SHOP_INFO.wifi.name}
|
||||
@@ -150,7 +150,7 @@ export default function Footer() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs tracking-wide uppercase opacity-60">
|
||||
Mật khẩu
|
||||
Password
|
||||
</span>
|
||||
<span className="border-opacity-30 rounded border border-(--color-accent) px-2 py-1 font-mono font-bold tracking-wider break-all text-(--color-accent)">
|
||||
{SHOP_INFO.wifi.password}
|
||||
@@ -170,9 +170,9 @@ export default function Footer() {
|
||||
© {new Date().getFullYear()} {SHOP_INFO.name}. All rights reserved.
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
Được vận hành{" "}
|
||||
Powered by{" "}
|
||||
<i className="fa-solid fa-heart mx-1 text-(--color-accent)"></i>{" "}
|
||||
bằng Drinkool
|
||||
Drinkool
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+9
-9
@@ -72,11 +72,11 @@ export default function Header() {
|
||||
/* Guest: sign-in button */
|
||||
<button
|
||||
onClick={handleAuthClick}
|
||||
title="Đăng nhập"
|
||||
title="Sign in"
|
||||
className="flex cursor-pointer items-center gap-2.5 rounded-xl border-none bg-(--color-primary) px-5 py-2.5 text-sm font-semibold text-white transition-all duration-150 hover:bg-(--color-primary-dark) active:scale-95"
|
||||
>
|
||||
<i className="fa-solid fa-right-to-bracket"></i>
|
||||
<span className="hidden sm:inline">Đăng nhập</span>
|
||||
<span className="hidden sm:inline">Sign in</span>
|
||||
</button>
|
||||
) : user.role === "manager" ? (
|
||||
/* Manager: dashboard link + logout */
|
||||
@@ -90,8 +90,8 @@ export default function Header() {
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleAuthClick}
|
||||
title="Đăng xuất"
|
||||
aria-label="Đăng xuất"
|
||||
title="Sign out"
|
||||
aria-label="Sign out"
|
||||
className="flex cursor-pointer items-center gap-2 rounded-xl border border-(--color-border) bg-transparent px-3 py-2.5 text-sm font-medium text-(--color-text-muted) transition-all duration-150 hover:border-red-300 hover:bg-red-50 hover:text-red-500 active:scale-95"
|
||||
>
|
||||
<i className="fa-solid fa-right-from-bracket text-base"></i>
|
||||
@@ -105,12 +105,12 @@ export default function Header() {
|
||||
className="flex items-center gap-2 rounded-xl border border-(--color-accent) bg-(--color-accent-light) px-4 py-2.5 text-sm font-semibold text-(--color-primary-dark) no-underline transition-all duration-150 hover:bg-(--color-accent) hover:text-white"
|
||||
>
|
||||
<i className="fa-solid fa-calendar-check text-base"></i>
|
||||
<span className="hidden sm:inline">Ca làm</span>
|
||||
<span className="hidden sm:inline">My Shifts</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleAuthClick}
|
||||
title="Nhấn để đăng xuất"
|
||||
aria-label="Đăng xuất"
|
||||
title="Click to sign out"
|
||||
aria-label="Sign out"
|
||||
className="bg-background flex cursor-pointer items-center gap-2.5 rounded-xl border border-(--color-border) px-4 py-2 text-sm font-semibold text-(--color-text-secondary) transition-all duration-150 hover:border-(--color-primary-light) hover:bg-(--color-border-light) active:scale-95"
|
||||
>
|
||||
{/* Avatar circle */}
|
||||
@@ -124,14 +124,14 @@ export default function Header() {
|
||||
/* Customer: phone icon + label */
|
||||
<button
|
||||
onClick={handleAuthClick}
|
||||
title={`Khách hàng - ${user.phone || ""} - Nhấn để đăng xuất`}
|
||||
title={`Customer - ${user.phone || ""} - Click to sign out`}
|
||||
className="flex cursor-pointer items-center gap-2.5 rounded-xl border-none bg-(--color-primary-light) px-4 py-2 text-sm font-semibold text-white transition-all duration-150 hover:bg-(--color-primary) active:scale-95"
|
||||
>
|
||||
{/* Customer icon */}
|
||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-white text-xs text-(--color-primary-light)">
|
||||
<i className="fa-solid fa-user"></i>
|
||||
</div>
|
||||
<span className="hidden sm:inline">Khách hàng</span>
|
||||
<span className="hidden sm:inline">Customer</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+96
-96
@@ -15,9 +15,9 @@ import type {
|
||||
// ===== SHOP INFORMATION =====
|
||||
export const SHOP_INFO: ShopInfo = {
|
||||
name: "Coffee Shop",
|
||||
tagline: "Hương vị đậm đà – Khoảnh khắc thư giãn",
|
||||
tagline: "Rich Flavors – Moments of Relaxation",
|
||||
logo: "/imgs/logo.png",
|
||||
address: "123 Đường Nguyễn Huệ, Quận 1, TP. Hồ Chí Minh",
|
||||
address: "123 Nguyen Hue Street, District 1, Ho Chi Minh City",
|
||||
phone: "0901 234 567",
|
||||
managerPhone: "0912 345 678",
|
||||
email: "contact@coffeeshop.vn",
|
||||
@@ -25,7 +25,7 @@ export const SHOP_INFO: ShopInfo = {
|
||||
name: "CoffeeShop_Free",
|
||||
password: "coffee2024",
|
||||
},
|
||||
openHours: "07:00 – 22:00 (Thứ 2 – Chủ nhật)",
|
||||
openHours: "07:00 – 22:00 (Monday – Sunday)",
|
||||
};
|
||||
|
||||
// ===== SOCIAL LINKS =====
|
||||
@@ -38,15 +38,15 @@ export const SOCIAL_LINKS: SocialLinks = {
|
||||
// ===== MENU CATEGORIES =====
|
||||
// Each category has a unique FontAwesome icon representing the item type
|
||||
export const MENU_CATEGORIES: MenuCategory[] = [
|
||||
{ id: "all", name: "Tất cả", icon: "fa-solid fa-border-all" },
|
||||
{ id: "cafe", name: "Cà Phê", icon: "fa-solid fa-mug-hot" },
|
||||
{ id: "tra", name: "Trà", icon: "fa-solid fa-leaf" },
|
||||
{ id: "sua-chua", name: "Sữa Chua", icon: "fa-solid fa-jar" },
|
||||
{ id: "nuoc-ep", name: "Nước Ép", icon: "fa-solid fa-blender" },
|
||||
{ id: "all", name: "All", icon: "fa-solid fa-border-all" },
|
||||
{ id: "cafe", name: "Coffee", icon: "fa-solid fa-mug-hot" },
|
||||
{ id: "tra", name: "Tea", icon: "fa-solid fa-leaf" },
|
||||
{ id: "sua-chua", name: "Yogurt", icon: "fa-solid fa-jar" },
|
||||
{ id: "nuoc-ep", name: "Juice", icon: "fa-solid fa-blender" },
|
||||
{ id: "latte", name: "Latte", icon: "fa-solid fa-mug-saucer" },
|
||||
{
|
||||
id: "giai-khat",
|
||||
name: "Giải Khát / Ăn Vặt",
|
||||
name: "Refreshments / Snacks",
|
||||
icon: "fa-solid fa-ice-cream",
|
||||
},
|
||||
{ id: "topping", name: "Topping", icon: "fa-solid fa-layer-group" },
|
||||
@@ -57,182 +57,182 @@ export const MENU_CATEGORIES: MenuCategory[] = [
|
||||
export const MOCK_PRODUCTS: Product[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Cà Phê Đen",
|
||||
name: "Black Coffee",
|
||||
category: "cafe",
|
||||
price: 25000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Cà phê đen truyền thống, đậm đà hương vị Việt Nam, pha phin thủ công.",
|
||||
"Traditional Vietnamese black coffee, rich and bold, brewed by hand with a phin filter.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Cà Phê Sữa",
|
||||
name: "Milk Coffee",
|
||||
category: "cafe",
|
||||
price: 30000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Cà phê sữa đặc thơm ngon, béo ngậy, kết hợp hoàn hảo giữa cà phê và sữa đặc.",
|
||||
"Aromatic condensed milk coffee, rich and creamy — a perfect blend of strong coffee and sweet condensed milk.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Bạc Xỉu",
|
||||
name: "White Coffee",
|
||||
category: "cafe",
|
||||
price: 32000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Bạc xỉu nhẹ nhàng, ít cà phê nhiều sữa, thích hợp cho người mới uống cà phê.",
|
||||
"Light and mild, with less coffee and more milk — perfect for those just starting to enjoy coffee.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Cà Phê Trứng",
|
||||
name: "Egg Coffee",
|
||||
category: "cafe",
|
||||
price: 45000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Cà phê trứng đặc sản Hà Nội, lớp kem trứng mịn màng phủ trên nền cà phê đậm đà.",
|
||||
"A Hanoi specialty — smooth, velvety egg cream layered over a bold coffee base.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Trà Đào Cam Sả",
|
||||
name: "Peach Orange Lemongrass Tea",
|
||||
category: "tra",
|
||||
price: 35000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Trà đào thơm mát kết hợp cam tươi và sả, thanh mát và giải nhiệt tuyệt vời.",
|
||||
"Fragrant peach tea with fresh orange and lemongrass — refreshing and wonderfully cooling.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Trà Xanh Matcha",
|
||||
name: "Matcha Green Tea",
|
||||
category: "tra",
|
||||
price: 40000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Matcha Nhật Bản nguyên chất, vị đắng nhẹ đặc trưng, thơm mát và bổ dưỡng.",
|
||||
"Pure Japanese matcha with a signature light bitterness — aromatic, refreshing, and nutritious.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Trà Vải Hoa Nhài",
|
||||
name: "Lychee Jasmine Tea",
|
||||
category: "tra",
|
||||
price: 38000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Trà vải thanh ngọt kết hợp hương hoa nhài dịu dàng, thư giãn tâm hồn.",
|
||||
"Sweet lychee tea delicately infused with jasmine blossom — a calming and soulful sip.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Sữa Chua Trân Châu",
|
||||
name: "Yogurt with Tapioca Pearls",
|
||||
category: "sua-chua",
|
||||
price: 38000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Sữa chua mịn màng kết hợp trân châu đen dẻo dai, chua ngọt hài hòa.",
|
||||
"Smooth, creamy yogurt paired with chewy black tapioca pearls — a perfectly balanced sweet and tangy treat.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Sữa Chua Dâu",
|
||||
name: "Strawberry Yogurt",
|
||||
category: "sua-chua",
|
||||
price: 40000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Sữa chua mát lạnh với dâu tươi ngọt chua, giàu vitamin và khoáng chất.",
|
||||
"Chilled yogurt with fresh sweet-tart strawberries, packed with vitamins and minerals.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Nước Ép Cam",
|
||||
name: "Fresh Orange Juice",
|
||||
category: "nuoc-ep",
|
||||
price: 35000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Nước ép cam tươi nguyên chất, giàu vitamin C, tốt cho sức khỏe.",
|
||||
"100% freshly squeezed orange juice, rich in vitamin C and great for your health.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Nước Ép Dưa Hấu",
|
||||
name: "Watermelon Juice",
|
||||
category: "nuoc-ep",
|
||||
price: 30000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Nước ép dưa hấu mát lạnh, giải nhiệt tức thì trong những ngày hè oi bức.",
|
||||
"Ice-cold watermelon juice — instantly refreshing on hot summer days.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Latte Caramel",
|
||||
name: "Caramel Latte",
|
||||
category: "latte",
|
||||
price: 45000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Latte caramel ngọt ngào, thơm béo với lớp foam sữa mịn và sốt caramel.",
|
||||
"Sweet and indulgent caramel latte with a velvety milk foam topping and a drizzle of caramel sauce.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Latte Vanilla",
|
||||
name: "Vanilla Latte",
|
||||
category: "latte",
|
||||
price: 45000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Latte vanilla nhẹ nhàng, hương thơm dịu dàng từ vanilla tự nhiên.",
|
||||
"Smooth and gentle vanilla latte with a delicate natural vanilla fragrance.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Bánh Mì Nướng Bơ",
|
||||
name: "Toasted Butter Bread",
|
||||
category: "giai-khat",
|
||||
price: 20000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Bánh mì nướng giòn rụm, phết bơ thơm và mứt dâu, ăn kèm cà phê tuyệt vời.",
|
||||
"Crispy toasted bread spread with fragrant butter and strawberry jam — the perfect coffee companion.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Bánh Flan",
|
||||
name: "Caramel Flan",
|
||||
category: "giai-khat",
|
||||
price: 25000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Bánh flan mềm mịn, ngọt ngào với lớp caramel vàng óng, tan chảy trong miệng.",
|
||||
"Silky smooth flan with a golden caramel topping that melts in your mouth.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "Trân Châu Đen",
|
||||
name: "Black Tapioca Pearls",
|
||||
category: "topping",
|
||||
price: 10000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Trân châu đen dẻo dai, thêm vào bất kỳ đồ uống nào để tăng thêm hương vị.",
|
||||
"Chewy black tapioca pearls — add them to any drink for extra flavor and texture.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: "Thạch Cà Phê",
|
||||
name: "Coffee Jelly",
|
||||
category: "topping",
|
||||
price: 10000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Thạch cà phê mát lạnh, thêm hương vị đặc biệt cho đồ uống của bạn.",
|
||||
"Cool coffee jelly that adds a distinctive flavor boost to your drink.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: "Trân Châu Trắng",
|
||||
name: "White Tapioca Pearls",
|
||||
category: "topping",
|
||||
price: 10000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description:
|
||||
"Trân châu trắng dẻo dai, thêm vào bất kỳ đồ uống nào để tăng thêm hương vị.",
|
||||
"Chewy white tapioca pearls — add them to any drink for extra flavor and texture.",
|
||||
available: true,
|
||||
},
|
||||
];
|
||||
@@ -241,8 +241,8 @@ export const MOCK_PRODUCTS: Product[] = [
|
||||
export const MOCK_COMBOS: Combo[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Combo Cà Phê Đôi",
|
||||
description: "2 ly cà phê đen + 2 bánh mì nướng bơ, tiết kiệm 15%.",
|
||||
name: "Double Coffee Combo",
|
||||
description: "2 black coffees + 2 toasted butter breads, save 15%.",
|
||||
price: 75000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
items: [
|
||||
@@ -253,8 +253,8 @@ export const MOCK_COMBOS: Combo[] = [
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Combo Trà Sữa Nhóm",
|
||||
description: "2 trà đào cam sả + 2 trà xanh matcha, dành cho nhóm bạn.",
|
||||
name: "Group Tea Combo",
|
||||
description: "2 peach orange lemongrass teas + 2 matcha green teas, perfect for a group.",
|
||||
price: 130000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
items: [
|
||||
@@ -265,8 +265,8 @@ export const MOCK_COMBOS: Combo[] = [
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Combo Buổi Sáng",
|
||||
description: "1 cà phê sữa + 1 bánh flan, khởi đầu ngày mới ngọt ngào.",
|
||||
name: "Morning Combo",
|
||||
description: "1 milk coffee + 1 caramel flan — start your day on a sweet note.",
|
||||
price: 48000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
items: [
|
||||
@@ -354,34 +354,34 @@ export const MOCK_REVENUE_DAILY: RevenueDataPoint[] = [
|
||||
|
||||
// Weekly revenue (last 12 weeks)
|
||||
export const MOCK_REVENUE_WEEKLY: RevenueDataPoint[] = [
|
||||
{ label: "T1/W1", revenue: 8200000, orders: 295 },
|
||||
{ label: "T1/W2", revenue: 9450000, orders: 340 },
|
||||
{ label: "T1/W3", revenue: 10100000, orders: 362 },
|
||||
{ label: "T1/W4", revenue: 8750000, orders: 315 },
|
||||
{ label: "T2/W1", revenue: 9200000, orders: 330 },
|
||||
{ label: "T2/W2", revenue: 10500000, orders: 378 },
|
||||
{ label: "T2/W3", revenue: 11200000, orders: 400 },
|
||||
{ label: "T2/W4", revenue: 9800000, orders: 352 },
|
||||
{ label: "T3/W1", revenue: 10400000, orders: 374 },
|
||||
{ label: "T3/W2", revenue: 11800000, orders: 424 },
|
||||
{ label: "T3/W3", revenue: 12500000, orders: 448 },
|
||||
{ label: "T3/W4", revenue: 10900000, orders: 392 },
|
||||
{ label: "Jan/W1", revenue: 8200000, orders: 295 },
|
||||
{ label: "Jan/W2", revenue: 9450000, orders: 340 },
|
||||
{ label: "Jan/W3", revenue: 10100000, orders: 362 },
|
||||
{ label: "Jan/W4", revenue: 8750000, orders: 315 },
|
||||
{ label: "Feb/W1", revenue: 9200000, orders: 330 },
|
||||
{ label: "Feb/W2", revenue: 10500000, orders: 378 },
|
||||
{ label: "Feb/W3", revenue: 11200000, orders: 400 },
|
||||
{ label: "Feb/W4", revenue: 9800000, orders: 352 },
|
||||
{ label: "Mar/W1", revenue: 10400000, orders: 374 },
|
||||
{ label: "Mar/W2", revenue: 11800000, orders: 424 },
|
||||
{ label: "Mar/W3", revenue: 12500000, orders: 448 },
|
||||
{ label: "Mar/W4", revenue: 10900000, orders: 392 },
|
||||
];
|
||||
|
||||
// Monthly revenue (last 12 months)
|
||||
export const MOCK_REVENUE_MONTHLY: RevenueDataPoint[] = [
|
||||
{ label: "T4/2025", revenue: 42000000, orders: 1512 },
|
||||
{ label: "T5/2025", revenue: 45500000, orders: 1638 },
|
||||
{ label: "T6/2025", revenue: 48000000, orders: 1728 },
|
||||
{ label: "T7/2025", revenue: 52000000, orders: 1872 },
|
||||
{ label: "T8/2025", revenue: 49500000, orders: 1782 },
|
||||
{ label: "T9/2025", revenue: 46800000, orders: 1685 },
|
||||
{ label: "T10/2025", revenue: 51200000, orders: 1843 },
|
||||
{ label: "T11/2025", revenue: 55000000, orders: 1980 },
|
||||
{ label: "T12/2025", revenue: 62000000, orders: 2232 },
|
||||
{ label: "T1/2026", revenue: 44000000, orders: 1584 },
|
||||
{ label: "T2/2026", revenue: 47500000, orders: 1710 },
|
||||
{ label: "T3/2026", revenue: 53500000, orders: 1926 },
|
||||
{ label: "Apr/2025", revenue: 42000000, orders: 1512 },
|
||||
{ label: "May/2025", revenue: 45500000, orders: 1638 },
|
||||
{ label: "Jun/2025", revenue: 48000000, orders: 1728 },
|
||||
{ label: "Jul/2025", revenue: 52000000, orders: 1872 },
|
||||
{ label: "Aug/2025", revenue: 49500000, orders: 1782 },
|
||||
{ label: "Sep/2025", revenue: 46800000, orders: 1685 },
|
||||
{ label: "Oct/2025", revenue: 51200000, orders: 1843 },
|
||||
{ label: "Nov/2025", revenue: 55000000, orders: 1980 },
|
||||
{ label: "Dec/2025", revenue: 62000000, orders: 2232 },
|
||||
{ label: "Jan/2026", revenue: 44000000, orders: 1584 },
|
||||
{ label: "Feb/2026", revenue: 47500000, orders: 1710 },
|
||||
{ label: "Mar/2026", revenue: 53500000, orders: 1926 },
|
||||
];
|
||||
|
||||
// Yearly revenue (last 5 years)
|
||||
@@ -397,7 +397,7 @@ export const MOCK_REVENUE_YEARLY: RevenueDataPoint[] = [
|
||||
export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
{
|
||||
productId: 12,
|
||||
name: "Latte Caramel",
|
||||
name: "Caramel Latte",
|
||||
category: "latte",
|
||||
unitsSold: 487,
|
||||
revenue: 21915000,
|
||||
@@ -408,7 +408,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 6,
|
||||
name: "Trà Xanh Matcha",
|
||||
name: "Matcha Green Tea",
|
||||
category: "tra",
|
||||
unitsSold: 412,
|
||||
revenue: 16480000,
|
||||
@@ -419,7 +419,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 5,
|
||||
name: "Trà Đào Cam Sả",
|
||||
name: "Peach Orange Lemongrass Tea",
|
||||
category: "tra",
|
||||
unitsSold: 398,
|
||||
revenue: 13930000,
|
||||
@@ -430,7 +430,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 13,
|
||||
name: "Latte Vanilla",
|
||||
name: "Vanilla Latte",
|
||||
category: "latte",
|
||||
unitsSold: 356,
|
||||
revenue: 16020000,
|
||||
@@ -441,7 +441,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 4,
|
||||
name: "Cà Phê Trứng",
|
||||
name: "Egg Coffee",
|
||||
category: "cafe",
|
||||
unitsSold: 340,
|
||||
revenue: 15300000,
|
||||
@@ -452,7 +452,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 2,
|
||||
name: "Cà Phê Sữa",
|
||||
name: "Milk Coffee",
|
||||
category: "cafe",
|
||||
unitsSold: 325,
|
||||
revenue: 9750000,
|
||||
@@ -463,7 +463,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 3,
|
||||
name: "Bạc Xỉu",
|
||||
name: "White Coffee",
|
||||
category: "cafe",
|
||||
unitsSold: 298,
|
||||
revenue: 9536000,
|
||||
@@ -474,7 +474,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 1,
|
||||
name: "Cà Phê Đen",
|
||||
name: "Black Coffee",
|
||||
category: "cafe",
|
||||
unitsSold: 285,
|
||||
revenue: 7125000,
|
||||
@@ -485,7 +485,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 9,
|
||||
name: "Sữa Chua Dâu",
|
||||
name: "Strawberry Yogurt",
|
||||
category: "sua-chua",
|
||||
unitsSold: 267,
|
||||
revenue: 10680000,
|
||||
@@ -496,7 +496,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 10,
|
||||
name: "Nước Ép Cam",
|
||||
name: "Fresh Orange Juice",
|
||||
category: "nuoc-ep",
|
||||
unitsSold: 241,
|
||||
revenue: 8435000,
|
||||
@@ -507,7 +507,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 8,
|
||||
name: "Sữa Chua Trân Châu",
|
||||
name: "Yogurt with Tapioca Pearls",
|
||||
category: "sua-chua",
|
||||
unitsSold: 228,
|
||||
revenue: 8664000,
|
||||
@@ -518,7 +518,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 7,
|
||||
name: "Trà Vải Hoa Nhài",
|
||||
name: "Lychee Jasmine Tea",
|
||||
category: "tra",
|
||||
unitsSold: 215,
|
||||
revenue: 8170000,
|
||||
@@ -529,7 +529,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 15,
|
||||
name: "Bánh Flan",
|
||||
name: "Caramel Flan",
|
||||
category: "giai-khat",
|
||||
unitsSold: 198,
|
||||
revenue: 4950000,
|
||||
@@ -540,7 +540,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 11,
|
||||
name: "Nước Ép Dưa Hấu",
|
||||
name: "Watermelon Juice",
|
||||
category: "nuoc-ep",
|
||||
unitsSold: 182,
|
||||
revenue: 5460000,
|
||||
@@ -551,7 +551,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 14,
|
||||
name: "Bánh Mì Nướng Bơ",
|
||||
name: "Toasted Butter Bread",
|
||||
category: "giai-khat",
|
||||
unitsSold: 175,
|
||||
revenue: 3500000,
|
||||
@@ -562,7 +562,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 16,
|
||||
name: "Trân Châu Đen",
|
||||
name: "Black Tapioca Pearls",
|
||||
category: "topping",
|
||||
unitsSold: 456,
|
||||
revenue: 4560000,
|
||||
@@ -573,7 +573,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 17,
|
||||
name: "Thạch Cà Phê",
|
||||
name: "Coffee Jelly",
|
||||
category: "topping",
|
||||
unitsSold: 389,
|
||||
revenue: 3890000,
|
||||
@@ -584,7 +584,7 @@ export const MOCK_PRODUCT_SALES: ProductSalesStats[] = [
|
||||
},
|
||||
{
|
||||
productId: 18,
|
||||
name: "Trân Châu Trắng",
|
||||
name: "White Tapioca Pearls",
|
||||
category: "topping",
|
||||
unitsSold: 342,
|
||||
revenue: 3420000,
|
||||
@@ -619,9 +619,9 @@ function generateMockShifts(): ShiftSlot[] {
|
||||
];
|
||||
|
||||
const staffPool = [
|
||||
{ id: 2, name: "Nguyễn Văn An" },
|
||||
{ id: 3, name: "Trần Thị Bình" },
|
||||
{ id: 4, name: "Lê Văn Cường" },
|
||||
{ id: 2, name: "Alex Nguyen" },
|
||||
{ id: 3, name: "Binh Tran" },
|
||||
{ id: 4, name: "Cuong Le" },
|
||||
];
|
||||
|
||||
// Generate shifts from April 6 to April 26, 2026
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Eatery } from "./types";
|
||||
|
||||
export async function gqlFetch<T = Record<string, unknown>>(
|
||||
query: string,
|
||||
variables?: Record<string, unknown>,
|
||||
@@ -12,3 +14,32 @@ export async function gqlFetch<T = Record<string, unknown>>(
|
||||
if (!res.ok) throw new Error(`GraphQL request failed: ${res.status}`);
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export async function addManagerMenuItem(
|
||||
name: string,
|
||||
price: number,
|
||||
): Promise<{ id: string; name: string; price: number } | null> {
|
||||
const data = await gqlFetch<{
|
||||
addMenuItem: { id: string; name: string; price: number } | null;
|
||||
}>(
|
||||
`mutation($name: String!, $price: Float!) {
|
||||
addMenuItem(name: $name, price: $price) { id name price }
|
||||
}`,
|
||||
{ name, price },
|
||||
);
|
||||
return data.addMenuItem;
|
||||
}
|
||||
|
||||
export async function fetchManagerEatery(): Promise<Eatery | null> {
|
||||
const data = await gqlFetch<{ eateriesByOwner: Eatery | null }>(`
|
||||
query {
|
||||
eateriesByOwner {
|
||||
id
|
||||
ownerId
|
||||
name
|
||||
isVerified
|
||||
}
|
||||
}
|
||||
`);
|
||||
return data.eateriesByOwner;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,14 @@ export interface SocialLinks {
|
||||
website: string;
|
||||
}
|
||||
|
||||
// ===== EATERY (GRAPHQL) TYPES =====
|
||||
export interface Eatery {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
name: string;
|
||||
isVerified: boolean;
|
||||
}
|
||||
|
||||
// ===== SHOP (QUÁN NƯỚC) TYPES =====
|
||||
export interface Shop {
|
||||
id: number;
|
||||
|
||||
Reference in New Issue
Block a user