chore: release [ci skip]

This commit is contained in:
gitea-actions
2026-05-14 15:57:15 +00:00
parent b37bf5d088
commit 9f8695a870
26 changed files with 509 additions and 262 deletions
+18 -6
View File
@@ -8,7 +8,9 @@ function formatWage(wage: number): string {
return wage.toLocaleString("vi-VN");
}
function getShiftPeriod(startTime: string): "morning" | "afternoon" | "evening" {
function getShiftPeriod(
startTime: string,
): "morning" | "afternoon" | "evening" {
const h = parseInt(startTime.split(":")[0] ?? "0", 10);
if (h < 12) return "morning";
if (h < 18) return "afternoon";
@@ -36,7 +38,11 @@ const PERIOD_STYLES = {
},
};
export default function ShiftCard({ shift, compact = false, onClick }: ShiftCardProps) {
export default function ShiftCard({
shift,
compact = false,
onClick,
}: ShiftCardProps) {
const registeredCount = shift.registeredStaff?.length ?? 0;
const period = getShiftPeriod(shift.startTime);
const s = PERIOD_STYLES[period];
@@ -49,12 +55,16 @@ export default function ShiftCard({ shift, compact = false, onClick }: ShiftCard
className={`group w-full cursor-pointer rounded-xl border-l-[3px] bg-white text-left shadow-sm transition hover:-translate-y-px hover:shadow-md ${s.border}`}
>
<div className="px-2.5 py-2">
<p className={`text-xs font-bold leading-tight ${s.text}`}>
<p className={`text-xs leading-tight font-bold ${s.text}`}>
{shift.startTime}{shift.endTime}
</p>
<div className="mt-1 flex items-center justify-between gap-1">
<p className="text-[10px] text-(--color-text-muted)">{formatWage(shift.wage ?? 0)}đ</p>
<span className={`rounded-full px-1.5 py-px text-[9px] font-semibold ${s.badge}`}>
<p className="text-[10px] text-(--color-text-muted)">
{formatWage(shift.wage ?? 0)}đ
</p>
<span
className={`rounded-full px-1.5 py-px text-[9px] font-semibold ${s.badge}`}
>
{registeredCount}/{shift.maxStaff}
</span>
</div>
@@ -82,7 +92,9 @@ export default function ShiftCard({ shift, compact = false, onClick }: ShiftCard
{(shift.wage ?? 0).toLocaleString("vi-VN")} VND/ca
</p>
</div>
<span className={`shrink-0 rounded-full px-2.5 py-1 text-xs font-semibold ${s.badge}`}>
<span
className={`shrink-0 rounded-full px-2.5 py-1 text-xs font-semibold ${s.badge}`}
>
{registeredCount}/{shift.maxStaff} staff
</span>
</div>
+4 -1
View File
@@ -46,7 +46,10 @@ export default function LoginForm() {
const role = (await res.text().catch(() => "")).trim().toLowerCase();
if (res.ok && (role === "customer" || role === "manager" || role === "staff")) {
if (
res.ok &&
(role === "customer" || role === "manager" || role === "staff")
) {
sessionStorage.setItem("login_phone", phone);
sessionStorage.setItem("login_role", role);
router.push(role === "customer" ? "/login/otp" : "/login/password");
@@ -17,7 +17,9 @@ export default function DeleteConfirm({
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
<i className="fa-solid fa-trash-can text-xl text-red-500"></i>
</div>
<h3 className="text-foreground text-base font-bold">Delete "{name}"?</h3>
<h3 className="text-foreground text-base font-bold">
Delete "{name}"?
</h3>
<p className="text-sm text-(--color-text-muted)">
This action cannot be undone.
</p>
+2 -2
View File
@@ -99,8 +99,8 @@ export default function ProductsTab() {
</div>
<p className="text-sm text-(--color-text-muted)">
Showing <strong className="text-foreground">{filtered.length}</strong>{" "}
/ {products.length} items
Showing <strong className="text-foreground">{filtered.length}</strong> /{" "}
{products.length} items
</p>
{/* Table */}
+1 -1
View File
@@ -142,7 +142,7 @@ export default function ReviewsTab() {
{review.comment}
</p>
) : (
<p className="text-sm italic text-(--color-text-muted)">
<p className="text-sm text-(--color-text-muted) italic">
No comment provided.
</p>
)}
+1 -3
View File
@@ -169,9 +169,7 @@ export default function ReviewModal({
</div>
{/* Error message */}
{error && (
<p className="mb-4 text-sm text-red-500">{error}</p>
)}
{error && <p className="mb-4 text-sm text-red-500">{error}</p>}
{/* Footer buttons */}
<div className="flex gap-3">
@@ -4,14 +4,14 @@ import { ProductCard } from "@/components/molecules/cards";
import { useCart } from "@/lib/cart-context";
import { useManager } from "@/lib/manager-context";
import type { ProductGridProps } from "./ProductGrid.types";
function toDisplayUrl(filename: string) {
if (!filename) return "/imgs/products/placeholder.jpg";
if (filename.startsWith("/") || filename.startsWith("http")) return filename;
return `/api/file/${filename}`;
}
import type { ProductGridProps } from "./ProductGrid.types";
export default function ProductGrid({
searchQuery = "",
isSidebarOpen = false,
@@ -8,8 +8,18 @@ import type { MobileShiftViewProps } from "./ShiftSchedule.types";
const DAY_HEADERS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const MONTH_NAMES = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
function formatDateKey(d: Date): string {
@@ -24,8 +34,11 @@ function isToday(d: Date): boolean {
return formatDateKey(d) === formatDateKey(today);
}
export default function MobileShiftView({ onShiftClick }: MobileShiftViewProps) {
const { currentDate, getShiftsForDate, goToNextMonth, goToPrevMonth } = useShift();
export default function MobileShiftView({
onShiftClick,
}: MobileShiftViewProps) {
const { currentDate, getShiftsForDate, goToNextMonth, goToPrevMonth } =
useShift();
const [selectedDate, setSelectedDate] = useState<Date>(new Date(2026, 3, 10));
const calendarDays = useMemo(() => {
@@ -39,12 +52,16 @@ export default function MobileShiftView({ onShiftClick }: MobileShiftViewProps)
const days: (Date | null)[] = [];
for (let i = 0; i < startOffset; i++) days.push(null);
for (let d = 1; d <= lastDay.getDate(); d++) days.push(new Date(year, month, d));
for (let d = 1; d <= lastDay.getDate(); d++)
days.push(new Date(year, month, d));
while (days.length % 7 !== 0) days.push(null);
return days;
}, [currentDate]);
const selectedShifts = useMemo(() => getShiftsForDate(selectedDate), [selectedDate, getShiftsForDate]);
const selectedShifts = useMemo(
() => getShiftsForDate(selectedDate),
[selectedDate, getShiftsForDate],
);
const dayOfWeek = DAY_HEADERS[(selectedDate.getDay() + 6) % 7];
@@ -96,7 +113,8 @@ export default function MobileShiftView({ onShiftClick }: MobileShiftViewProps)
if (!date) return <div key={`empty-${i}`} className="p-1" />;
const today = isToday(date);
const selected = formatDateKey(date) === formatDateKey(selectedDate);
const selected =
formatDateKey(date) === formatDateKey(selectedDate);
const dayShifts = getShiftsForDate(date);
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
@@ -136,14 +154,16 @@ export default function MobileShiftView({ onShiftClick }: MobileShiftViewProps)
<div>
<div className="mb-3 flex items-center justify-between px-1">
<h3 className="text-sm font-bold text-(--color-primary-dark)">
{dayOfWeek},{" "}
{selectedDate.getDate()}/{selectedDate.getMonth() + 1}/{selectedDate.getFullYear()}
{dayOfWeek}, {selectedDate.getDate()}/{selectedDate.getMonth() + 1}/
{selectedDate.getFullYear()}
</h3>
<span className={`rounded-full px-2.5 py-1 text-xs font-semibold ${
selectedShifts.length > 0
? "bg-(--color-primary)/10 text-(--color-primary)"
: "bg-gray-100 text-(--color-text-muted)"
}`}>
<span
className={`rounded-full px-2.5 py-1 text-xs font-semibold ${
selectedShifts.length > 0
? "bg-(--color-primary)/10 text-(--color-primary)"
: "bg-gray-100 text-(--color-text-muted)"
}`}
>
{selectedShifts.length} shifts
</span>
</div>
@@ -151,8 +171,12 @@ export default function MobileShiftView({ onShiftClick }: MobileShiftViewProps)
{selectedShifts.length === 0 ? (
<div className="rounded-2xl border border-dashed border-(--color-border-light) py-10 text-center">
<i className="fa-regular fa-calendar-xmark mb-3 text-3xl text-gray-200"></i>
<p className="text-sm font-medium text-(--color-text-muted)">No shifts</p>
<p className="mt-0.5 text-xs text-(--color-text-muted)">No shifts created for this day</p>
<p className="text-sm font-medium text-(--color-text-muted)">
No shifts
</p>
<p className="mt-0.5 text-xs text-(--color-text-muted)">
No shifts created for this day
</p>
</div>
) : (
<div className="space-y-2.5">
@@ -23,7 +23,10 @@ function isToday(d: Date): boolean {
);
}
export default function MonthlyCalendar({ onShiftClick, onDateSelect }: MonthlyCalendarProps) {
export default function MonthlyCalendar({
onShiftClick,
onDateSelect,
}: MonthlyCalendarProps) {
const { currentDate, getShiftsForDate } = useShift();
const calendarDays = useMemo(() => {
@@ -37,7 +40,8 @@ export default function MonthlyCalendar({ onShiftClick, onDateSelect }: MonthlyC
const days: (Date | null)[] = [];
for (let i = 0; i < startOffset; i++) days.push(null);
for (let d = 1; d <= lastDay.getDate(); d++) days.push(new Date(year, month, d));
for (let d = 1; d <= lastDay.getDate(); d++)
days.push(new Date(year, month, d));
while (days.length % 7 !== 0) days.push(null);
return days;
}, [currentDate]);
@@ -21,8 +21,16 @@ function TimeInput({
const hour12 = h24 === 0 ? 12 : h24 > 12 ? h24 - 12 : h24;
const emit = (newH12: number, newM: number, newIsPM: boolean) => {
let h = newIsPM ? (newH12 === 12 ? 12 : newH12 + 12) : (newH12 === 12 ? 0 : newH12);
onChange(`${h.toString().padStart(2, "0")}:${newM.toString().padStart(2, "0")}`);
let h = newIsPM
? newH12 === 12
? 12
: newH12 + 12
: newH12 === 12
? 0
: newH12;
onChange(
`${h.toString().padStart(2, "0")}:${newM.toString().padStart(2, "0")}`,
);
};
return (
@@ -33,23 +41,27 @@ function TimeInput({
min={1}
max={12}
value={hour12}
onChange={(e) => emit(Math.min(12, Math.max(1, Number(e.target.value))), m, isPM)}
onChange={(e) =>
emit(Math.min(12, Math.max(1, Number(e.target.value))), m, isPM)
}
className="text-foreground w-12 border-none bg-transparent py-2.5 pl-3 text-sm outline-none"
/>
<span className="select-none text-sm text-(--color-text-muted)">:</span>
<span className="text-sm text-(--color-text-muted) select-none">:</span>
<input
title={`${title} minute`}
type="number"
min={0}
max={59}
value={m.toString().padStart(2, "0")}
onChange={(e) => emit(hour12, Math.min(59, Math.max(0, Number(e.target.value))), isPM)}
onChange={(e) =>
emit(hour12, Math.min(59, Math.max(0, Number(e.target.value))), isPM)
}
className="text-foreground w-10 border-none bg-transparent py-2.5 text-sm outline-none"
/>
<button
type="button"
onClick={() => emit(hour12, m, !isPM)}
className="ml-1 mr-2 cursor-pointer rounded-lg border-none bg-(--color-primary)/10 px-2 py-1 text-xs font-bold text-(--color-primary) transition hover:bg-(--color-primary) hover:text-white"
className="mr-2 ml-1 cursor-pointer rounded-lg border-none bg-(--color-primary)/10 px-2 py-1 text-xs font-bold text-(--color-primary) transition hover:bg-(--color-primary) hover:text-white"
>
{isPM ? "PM" : "AM"}
</button>
@@ -32,7 +32,9 @@ export default function ShiftDetailModal({
// const dept = DEPARTMENTS.find((d) => d.id === shift.department);
const isManager = user?.role === "manager";
const registeredStaff = shift.registeredStaff ?? [];
const isRegistered = user ? registeredStaff.some((s) => s.id === user.id) : false;
const isRegistered = user
? registeredStaff.some((s) => s.id === user.id)
: false;
const isFull = registeredStaff.length >= shift.maxStaff;
const handleRegister = async () => {
@@ -126,15 +128,14 @@ export default function ShiftDetailModal({
Date
</p>
<p className="text-foreground mt-1 text-sm font-bold">
{parseShiftDate(shift.date as Date | string | undefined)?.toLocaleDateString(
"vi-VN",
{
weekday: "long",
day: "2-digit",
month: "2-digit",
year: "numeric",
},
) ?? "—"}
{parseShiftDate(
shift.date as Date | string | undefined,
)?.toLocaleDateString("vi-VN", {
weekday: "long",
day: "2-digit",
month: "2-digit",
year: "numeric",
}) ?? "—"}
</p>
</div>
<div className="rounded-xl bg-gray-50 p-3">
@@ -164,8 +165,7 @@ export default function ShiftDetailModal({
{/* Registered staff */}
<div>
<p className="mb-2 text-xs font-semibold text-(--color-text-secondary)">
Registered staff ({registeredStaff.length}/
{shift.maxStaff})
Registered staff ({registeredStaff.length}/{shift.maxStaff})
</p>
{registeredStaff.length === 0 ? (
<p className="text-xs text-(--color-text-muted) italic">
@@ -8,8 +8,18 @@ import { useMemo, useState } from "react";
import type { WeeklyScheduleProps } from "./ShiftSchedule.types";
const MONTH_NAMES = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const DAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
@@ -32,13 +42,23 @@ export default function WeeklySchedule({
onCreateShift,
mobileCalendarHeader = false,
}: WeeklyScheduleProps) {
const { currentDate, getWeekDates, getShiftsForDate, goToNextWeek, goToPrevWeek } = useShift();
const {
currentDate,
getWeekDates,
getShiftsForDate,
goToNextWeek,
goToPrevWeek,
} = useShift();
const weekDates = getWeekDates();
const [selectedDate, setSelectedDate] = useState<Date>(weekDates[0] ?? currentDate);
const [selectedDate, setSelectedDate] = useState<Date>(
weekDates[0] ?? currentDate,
);
const selectedDateRef = useMemo(() => {
return weekDates.find((d) => d === selectedDate) ?? weekDates[0] ?? currentDate;
return (
weekDates.find((d) => d === selectedDate) ?? weekDates[0] ?? currentDate
);
}, [selectedDate, weekDates, currentDate]);
// ── Mobile calendar header view ────────────────────────────────────────────
@@ -83,18 +103,22 @@ export default function WeeklySchedule({
onClick={() => setSelectedDate(date)}
className="flex cursor-pointer flex-col items-center gap-1 rounded-xl border-none bg-transparent py-1.5 transition"
>
<span className={`text-[10px] font-semibold uppercase ${i >= 5 ? "text-rose-400" : "text-(--color-text-muted)"}`}>
<span
className={`text-[10px] font-semibold uppercase ${i >= 5 ? "text-rose-400" : "text-(--color-text-muted)"}`}
>
{DAY_LABELS[i]}
</span>
<span className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold transition ${
today
? "bg-(--color-primary) text-white shadow-sm"
: active
? "bg-(--color-primary)/15 text-(--color-primary)"
: i >= 5
? "text-rose-500"
: "text-(--color-text-secondary)"
}`}>
<span
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold transition ${
today
? "bg-(--color-primary) text-white shadow-sm"
: active
? "bg-(--color-primary)/15 text-(--color-primary)"
: i >= 5
? "text-rose-500"
: "text-(--color-text-secondary)"
}`}
>
{new Date(date).getDate()}
</span>
<div className="flex min-h-2 items-center gap-px">
@@ -123,12 +147,18 @@ export default function WeeklySchedule({
{dayShifts.length === 0 && !onCreateShift ? (
<div className="rounded-2xl border border-dashed border-(--color-border-light) py-10 text-center">
<i className="fa-regular fa-calendar-xmark mb-2 text-3xl text-gray-200"></i>
<p className="text-sm text-(--color-text-muted)">No shifts today</p>
<p className="text-sm text-(--color-text-muted)">
No shifts today
</p>
</div>
) : (
<div className="space-y-2.5">
{dayShifts.map((shift) => (
<ShiftCard key={shift.id} shift={shift} onClick={onShiftClick} />
<ShiftCard
key={shift.id}
shift={shift}
onClick={onShiftClick}
/>
))}
{onCreateShift && (
<button
@@ -169,18 +199,23 @@ export default function WeeklySchedule({
: "bg-gray-50/80 font-semibold text-(--color-text-muted)"
}`}
>
<span className="block text-[11px] font-semibold uppercase tracking-wider">
<span className="block text-[11px] font-semibold tracking-wider uppercase">
{DAY_LABELS[i]}
</span>
<span className={`mt-0.5 inline-flex h-7 w-7 items-center justify-center rounded-full text-sm font-bold ${
today
? "bg-(--color-primary) text-white shadow-sm"
: "text-(--color-text-secondary)"
}`}>
<span
className={`mt-0.5 inline-flex h-7 w-7 items-center justify-center rounded-full text-sm font-bold ${
today
? "bg-(--color-primary) text-white shadow-sm"
: "text-(--color-text-secondary)"
}`}
>
{date.getDate()}
</span>
<span className={`mt-0.5 block text-[10px] font-normal ${today ? "text-(--color-primary)/70" : "text-(--color-text-muted)"}`}>
{(date.getMonth() + 1).toString().padStart(2, "0")}/{date.getFullYear()}
<span
className={`mt-0.5 block text-[10px] font-normal ${today ? "text-(--color-primary)/70" : "text-(--color-text-muted)"}`}
>
{(date.getMonth() + 1).toString().padStart(2, "0")}/
{date.getFullYear()}
</span>
</th>
);
@@ -189,11 +224,16 @@ export default function WeeklySchedule({
</thead>
<tbody>
{DEPARTMENTS.map((dept, deptIdx) => (
<tr key={dept.id} className={deptIdx % 2 === 0 ? "bg-white" : "bg-gray-50/40"}>
<tr
key={dept.id}
className={deptIdx % 2 === 0 ? "bg-white" : "bg-gray-50/40"}
>
<td className="border-r border-b border-(--color-border-light) px-4 py-3 align-top">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-(--color-primary)/10">
<i className={`${dept.icon} text-xs text-(--color-primary)`}></i>
<i
className={`${dept.icon} text-xs text-(--color-primary)`}
></i>
</div>
<span className="text-xs font-semibold text-(--color-text-secondary)">
{dept.name}
@@ -212,7 +252,12 @@ export default function WeeklySchedule({
>
<div className="flex min-h-[88px] flex-col gap-1.5">
{shifts.map((shift) => (
<ShiftCard key={shift.id} shift={shift} compact onClick={onShiftClick} />
<ShiftCard
key={shift.id}
shift={shift}
compact
onClick={onShiftClick}
/>
))}
{onCreateShift && (
<button