diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index b4aeb7a..a42775c 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -95,7 +95,6 @@ jobs: password: ${{ secrets.ACCESS_TOKEN }} - name: Prepare Docker Metadata - if: gitea.ref == 'refs/heads/main' id: meta run: | # Lowercase Repository @@ -108,11 +107,10 @@ jobs: echo "version=$VERSION_LOWER" >> $GITHUB_OUTPUT - name: Build and Push Docker Image - if: gitea.ref == 'refs/heads/main' uses: docker/build-push-action@v4 with: context: . - push: true + push: ${{ github.ref_name == 'main' }} tags: | vps.demonkernel.io.vn/${{ steps.meta.outputs.repo }}:latest vps.demonkernel.io.vn/${{ steps.meta.outputs.repo }}:${{ steps.meta.outputs.version }} diff --git a/app/(feed)/feed/page.tsx b/app/(feed)/feed/page.tsx deleted file mode 100644 index ab70930..0000000 --- a/app/(feed)/feed/page.tsx +++ /dev/null @@ -1,86 +0,0 @@ -"use client"; - -import { SearchBar } from "@/components/molecules/search-bar"; -import { ShopGrid } from "@/components/organisms/shop-grid"; -import { useState } from "react"; - -export default function FeedPage() { - const [searchName, setSearchName] = useState(""); - const [searchAddress, setSearchAddress] = useState(""); - - const hasFilters = searchName || searchAddress; - - return ( - - - {/* Page title */} - - - Explore Shops - - - Find and choose your favorite shop - - - - {/* Shop grid */} - - - {hasFilters && ( - - { - setSearchName(""); - setSearchAddress(""); - }} - className="cursor-pointer border-none bg-transparent text-sm text-(--color-primary) hover:underline" - > - Clear filters - - - )} - - - {/* Filter / Search bar — sticky bottom */} - - - - - Filter shops - - - {/* Name search */} - setSearchName("")} - placeholder="Search by shop name..." - className="min-w-0 flex-1" - /> - - {/* Address search — different icon so not using SearchBar atom */} - - - setSearchAddress(e.target.value)} - 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 && ( - setSearchAddress("")} - 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)" - > - - - )} - - - - - - ); -} diff --git a/app/(feed)/layout.tsx b/app/(feed)/layout.tsx deleted file mode 100644 index 575cada..0000000 --- a/app/(feed)/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { FeedLayout } from "@/components/templates/feed-layout"; - -export default function RootFeedLayout({ - children, -}: { - children: React.ReactNode; -}) { - return {children}; -} diff --git a/app/(staff)/staff/schedule/page.tsx b/app/(staff)/staff/schedule/page.tsx index 625f8ec..f1cede4 100644 --- a/app/(staff)/staff/schedule/page.tsx +++ b/app/(staff)/staff/schedule/page.tsx @@ -7,7 +7,7 @@ import ShiftDetailModal from "@/components/organisms/shift-schedule/ShiftDetailM import WeeklySchedule from "@/components/organisms/shift-schedule/WeeklySchedule"; import { useAuth } from "@/lib/auth-context"; import { useShift } from "@/lib/shift-context"; -import type { ShiftSlot } from "@/lib/types"; +import type { ShiftEntity } from "@/lib/types"; import Link from "next/link"; import { useState } from "react"; @@ -52,24 +52,24 @@ export default function StaffSchedulePage() { getWeeklyBudget, } = useShift(); - const [selectedShift, setSelectedShift] = useState(null); + const [selectedShift, setSelectedShift] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false); - const [createDate, setCreateDate] = useState(); + const [createDate, setCreateDate] = useState(); const isManager = user?.role === "manager"; - const handleShiftClick = (shift: ShiftSlot) => { + const handleShiftClick = (shift: ShiftEntity) => { setSelectedShift(shift); setDetailOpen(true); }; - const handleCreateShift = (date: string) => { + const handleCreateShift = (date: Date) => { setCreateDate(date); setCreateOpen(true); }; - const handleDateSelect = (date: string) => { + const handleDateSelect = (date: Date) => { // In month view on desktop, clicking a date could open create modal for managers if (isManager) { setCreateDate(date); diff --git a/components/molecules/cards/ShiftCard.tsx b/components/molecules/cards/ShiftCard.tsx index 479f726..217b9df 100644 --- a/components/molecules/cards/ShiftCard.tsx +++ b/components/molecules/cards/ShiftCard.tsx @@ -1,35 +1,9 @@ "use client"; -import type { ShiftSlot } from "@/lib/types"; +import type { ShiftEntity } from "@/lib/types"; import type { ShiftCardProps } from "./ShiftCard.types"; -const STATUS_STYLES: Record< - ShiftSlot["status"], - { bg: string; text: string; label: string } -> = { - available: { - bg: "bg-blue-50 border-blue-200", - text: "text-blue-700", - label: "Còn trống", - }, - registered: { - bg: "bg-blue-100 border-blue-400", - text: "text-blue-900", - label: "Đã đăng ký", - }, - approved_leave: { - bg: "bg-purple-50 border-purple-300", - text: "text-purple-700", - label: "Nghỉ phép", - }, - absent: { - bg: "bg-red-50 border-red-300", - text: "text-red-700", - label: "Vắng mặt", - }, -}; - function formatWage(wage: number): string { if (wage >= 1000) { return `${(wage / 1000).toFixed(0)}k`; @@ -42,20 +16,20 @@ export default function ShiftCard({ compact = false, onClick, }: ShiftCardProps) { - const style = STATUS_STYLES[shift.status]; + console.log(shift); if (compact) { return ( onClick?.(shift)} - className={`w-full cursor-pointer rounded-lg border px-2 py-1.5 text-left text-xs transition-shadow hover:shadow-sm ${style.bg} ${style.text}`} + className={`w-full cursor-pointer rounded-lg border px-2 py-1.5 text-left text-xs transition-shadow hover:shadow-sm `} > {shift.startTime} – {shift.endTime} - {shift.durationHours}h · {formatWage(shift.wage)} + {}h · {formatWage(shift.wage)} ); @@ -65,7 +39,7 @@ export default function ShiftCard({ onClick?.(shift)} - className={`w-full cursor-pointer rounded-xl border p-3 text-left transition-shadow hover:shadow-md ${style.bg} ${style.text}`} + className={`w-full cursor-pointer rounded-xl border p-3 text-left transition-shadow hover:shadow-md `} > @@ -73,25 +47,26 @@ export default function ShiftCard({ {shift.startTime} – {shift.endTime} - {shift.durationHours}h · {formatWage(shift.wage)} VND + {}h · {formatWage(shift.wage)} VND - {style.label} + {/* {style.label} */} - {shift.registeredStaff.length > 0 && ( + {(shift.registeredStaff && shift.registeredStaff.length > 0) && ( Nhân viên ({shift.registeredStaff.length}/{shift.maxStaff}) @@ -102,18 +77,18 @@ export default function ShiftCard({ key={s.id} className="rounded-full bg-white/60 px-2 py-0.5 text-[10px] font-medium" > - {s.name} + {s.staffId} ))} )} - {shift.status === "available" && shift.registeredStaff.length === 0 && ( + {/* {shift.status === "available" && shift.registeredStaff.length === 0 && ( {shift.maxStaff} vị trí còn trống - )} + )} */} ); } diff --git a/components/molecules/cards/ShiftCard.types.ts b/components/molecules/cards/ShiftCard.types.ts index 47f0c02..1723b78 100644 --- a/components/molecules/cards/ShiftCard.types.ts +++ b/components/molecules/cards/ShiftCard.types.ts @@ -1,7 +1,7 @@ -import type { ShiftSlot } from "@/lib/types"; +import type { ShiftEntity } from "@/lib/types"; export interface ShiftCardProps { - shift: ShiftSlot; + shift: ShiftEntity; compact?: boolean; - onClick?: (shift: ShiftSlot) => void; + onClick?: (shift: ShiftEntity) => void; } diff --git a/components/organisms/index.ts b/components/organisms/index.ts index ff8605e..2a94e25 100644 --- a/components/organisms/index.ts +++ b/components/organisms/index.ts @@ -15,7 +15,7 @@ export { ReviewModal } from "./modals"; export type { ReviewModalProps, ConfirmModalProps } from "./modals"; // Shop Grid -export { ShopGrid } from "./shop-grid"; +// export { ShopGrid } from "./shop-grid"; export type { ShopGridProps } from "./shop-grid"; // Manager diff --git a/components/organisms/shift-schedule/MobileShiftView.tsx b/components/organisms/shift-schedule/MobileShiftView.tsx index 5da3fc3..7a7254b 100644 --- a/components/organisms/shift-schedule/MobileShiftView.tsx +++ b/components/organisms/shift-schedule/MobileShiftView.tsx @@ -44,9 +44,7 @@ export default function MobileShiftView({ onShiftClick, }: MobileShiftViewProps) { const { currentDate, shifts, goToNextMonth, goToPrevMonth } = useShift(); - const [selectedDate, setSelectedDate] = useState( - formatDateISO(new Date(2026, 3, 10)), - ); + const [selectedDate, setSelectedDate] = useState(new Date(2026, 3, 10)); const calendarDays = useMemo(() => { const year = currentDate.getFullYear(); @@ -67,21 +65,20 @@ export default function MobileShiftView({ }, [currentDate]); const getDotColors = (date: Date): string[] => { - const dateStr = formatDateISO(date); - const dayShifts = shifts.filter((s) => s.date === dateStr); + const dayShifts = shifts.filter((s) => s.date.getDate() === date.getDate()); const dots: string[] = []; - if (dayShifts.some((s) => s.status === "available")) - dots.push("bg-amber-400"); - if (dayShifts.some((s) => s.status === "registered")) - dots.push("bg-green-500"); - if (dayShifts.some((s) => s.status === "approved_leave")) - dots.push("bg-purple-400"); - if (dayShifts.some((s) => s.status === "absent")) dots.push("bg-red-400"); + // if (dayShifts.some((s) => s.status === "available")) + // dots.push("bg-amber-400"); + // if (dayShifts.some((s) => s.status === "registered")) + // dots.push("bg-green-500"); + // if (dayShifts.some((s) => s.status === "approved_leave")) + // dots.push("bg-purple-400"); + // if (dayShifts.some((s) => s.status === "absent")) dots.push("bg-red-400"); return dots; }; const selectedShifts = useMemo(() => { - return shifts.filter((s) => s.date === selectedDate); + return shifts.filter((s) => s.date.getDate() === selectedDate.getDate()); }, [shifts, selectedDate]); const selectedDateObj = new Date(selectedDate + "T00:00:00"); @@ -133,16 +130,15 @@ export default function MobileShiftView({ return ; } - const dateStr = formatDateISO(date); const today = isToday(date); - const selected = dateStr === selectedDate; + const selected = date === selectedDate; const dots = getDotColors(date); return ( setSelectedDate(dateStr)} + onClick={() => setSelectedDate(date)} className={`flex cursor-pointer flex-col items-center border-none bg-transparent p-1 transition ${ selected ? "rounded-lg bg-(--color-primary)/10" : "" }`} @@ -218,9 +214,7 @@ export default function MobileShiftView({ ) : ( {DEPARTMENTS.map((dept) => { - const deptShifts = selectedShifts.filter( - (s) => s.department === dept.id, - ); + const deptShifts = selectedShifts; if (deptShifts.length === 0) return null; return ( diff --git a/components/organisms/shift-schedule/MonthlyCalendar.tsx b/components/organisms/shift-schedule/MonthlyCalendar.tsx index 0bcb332..77f2981 100644 --- a/components/organisms/shift-schedule/MonthlyCalendar.tsx +++ b/components/organisms/shift-schedule/MonthlyCalendar.tsx @@ -7,7 +7,7 @@ import type { MonthlyCalendarProps } from "./ShiftSchedule.types"; const DAY_HEADERS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; -function formatDateISO(d: Date): string { +export function formatDateISO(d: Date): string { const y = d.getFullYear(); const m = (d.getMonth() + 1).toString().padStart(2, "0"); const day = d.getDate().toString().padStart(2, "0"); @@ -60,15 +60,14 @@ export default function MonthlyCalendar({ }, [currentDate]); const getShiftSummary = (date: Date) => { - const dateStr = formatDateISO(date); - const dayShifts = shifts.filter((s) => s.date === dateStr); - const available = dayShifts.filter((s) => s.status === "available").length; - const registered = dayShifts.filter( - (s) => s.status === "registered", - ).length; - const leave = dayShifts.filter((s) => s.status === "approved_leave").length; - const absent = dayShifts.filter((s) => s.status === "absent").length; - return { total: dayShifts.length, available, registered, leave, absent }; + const dayShifts = shifts.filter((s) => s.date.getDate() === date.getDate()); + // const available = dayShifts.filter((s) => s.status === "available").length; + // const registered = dayShifts.filter( + // (s) => s.status === "registered", + // ).length; + // const leave = dayShifts.filter((s) => s.status === "approved_leave").length; + // const absent = dayShifts.filter((s) => s.status === "absent").length; + return { total: dayShifts.length }; }; return ( @@ -104,7 +103,7 @@ export default function MonthlyCalendar({ onDateSelect?.(formatDateISO(date))} + onClick={() => onDateSelect?.(date)} className={`min-h-25 cursor-pointer border-r border-b border-(--color-border-light) bg-transparent p-2 text-left transition hover:bg-gray-50 ${ today ? "bg-(--color-primary)/5" : "" }`} @@ -119,7 +118,7 @@ export default function MonthlyCalendar({ {date.getDate()} - {summary.total > 0 && ( + {/* {summary.total > 0 && ( {summary.available > 0 && ( @@ -154,7 +153,7 @@ export default function MonthlyCalendar({ )} - )} + )} */} ); })} diff --git a/components/organisms/shift-schedule/ShiftCreateModal.tsx b/components/organisms/shift-schedule/ShiftCreateModal.tsx index 9a0dddb..91ec295 100644 --- a/components/organisms/shift-schedule/ShiftCreateModal.tsx +++ b/components/organisms/shift-schedule/ShiftCreateModal.tsx @@ -4,6 +4,7 @@ import { DEPARTMENTS } from "@/lib/constants"; import { useShift } from "@/lib/shift-context"; import { useEffect, useState } from "react"; +import { formatDateISO } from "./MonthlyCalendar"; import type { ShiftCreateModalProps } from "./ShiftSchedule.types"; export default function ShiftCreateModal({ @@ -13,7 +14,7 @@ export default function ShiftCreateModal({ }: ShiftCreateModalProps) { const { createShift } = useShift(); - const [date, setDate] = useState(defaultDate ?? "2026-04-10"); + const [date, setDate] = useState(new Date(defaultDate ?? "2026-04-10")); const [startTime, setStartTime] = useState("08:00"); const [endTime, setEndTime] = useState("12:00"); const [department, setDepartment] = useState("bar"); @@ -23,7 +24,7 @@ export default function ShiftCreateModal({ useEffect(() => { if (isOpen) { - setDate(defaultDate ?? "2026-04-10"); + setDate(new Date(defaultDate ?? "2026-04-10")); } }, [defaultDate, isOpen]); @@ -49,18 +50,12 @@ export default function ShiftCreateModal({ return; } - const durationHours = (endMinutes - startMinutes) / 60; - createShift({ date, startTime, endTime, - durationHours, wage, - department, maxStaff, - registeredStaff: [], - status: "available", }); onClose(); @@ -103,8 +98,8 @@ export default function ShiftCreateModal({ setDate(e.target.value)} + value={formatDateISO(date)} + onChange={(e) => setDate(new Date(e.target.value))} className="text-foreground w-full rounded-xl border border-(--color-border-light) px-3 py-2.5 text-sm transition outline-none focus:border-(--color-primary) focus:ring-1 focus:ring-(--color-primary)" /> diff --git a/components/organisms/shift-schedule/ShiftDetailModal.tsx b/components/organisms/shift-schedule/ShiftDetailModal.tsx index b513612..45105a9 100644 --- a/components/organisms/shift-schedule/ShiftDetailModal.tsx +++ b/components/organisms/shift-schedule/ShiftDetailModal.tsx @@ -19,12 +19,12 @@ export default function ShiftDetailModal({ if (!isOpen || !shift) return null; - const dept = DEPARTMENTS.find((d) => d.id === shift.department); + // const dept = DEPARTMENTS.find((d) => d.id === shift.department); const isManager = user?.role === "manager"; const isRegistered = user - ? shift.registeredStaff.some((s) => s.id === user.id) + ? shift.registeredStaff!.some((s) => s.id === user.id) : false; - const isFull = shift.registeredStaff.length >= shift.maxStaff; + const isFull = shift.registeredStaff!.length >= shift.maxStaff; const handleRegister = () => { if (!user) return; @@ -47,7 +47,7 @@ export default function ShiftDetailModal({ setTimeout(onClose, 1200); }; - const handleManagerUnregister = (staffId: number) => { + const handleManagerUnregister = (staffId: string) => { unregisterShift(shift.id, staffId); setSuccess("Đã xóa nhân viên khỏi ca."); }; @@ -81,12 +81,12 @@ export default function ShiftDetailModal({ {/* Header */} - {dept && } + {/* {dept && } */} Chi tiết ca làm - {dept?.name} + {/* {dept?.name} */} {/* Status badge */} - + {/* {statusLabel[shift.status]} - + */} {/* Shift info grid */} @@ -141,7 +141,7 @@ export default function ShiftDetailModal({ Thời lượng - {shift.durationHours} giờ + {""} giờ @@ -157,16 +157,16 @@ export default function ShiftDetailModal({ {/* Registered staff */} - Nhân viên đã đăng ký ({shift.registeredStaff.length}/ + Nhân viên đã đăng ký ({shift.registeredStaff!.length}/ {shift.maxStaff}) - {shift.registeredStaff.length === 0 ? ( + {(shift.registeredStaff && shift.registeredStaff.length === 0) ? ( Chưa có ai đăng ký ) : ( - {shift.registeredStaff.map((staff) => ( + {shift.registeredStaff!.map((staff) => ( - {staff.name} + {staff.staffId} {isManager && ( @@ -212,7 +212,7 @@ export default function ShiftDetailModal({ {/* Footer actions */} - {!isRegistered && + {/* {!isRegistered && !isFull && shift.status !== "approved_leave" && shift.status !== "absent" && ( @@ -224,7 +224,7 @@ export default function ShiftDetailModal({ Đăng ký ca - )} + )} */} {isRegistered && ( void; - onCreateShift?: (date: string) => void; + onShiftClick: (shift: ShiftEntity) => void; + onCreateShift?: (date: Date) => void; mobileCalendarHeader?: boolean; } export interface MonthlyCalendarProps { - onShiftClick: (shift: ShiftSlot) => void; - onDateSelect?: (date: string) => void; + onShiftClick: (shift: ShiftEntity) => void; + onDateSelect?: (date: Date) => void; } export interface MobileShiftViewProps { - onShiftClick: (shift: ShiftSlot) => void; + onShiftClick: (shift: ShiftEntity) => void; } export interface ShiftDetailModalProps { - shift: ShiftSlot | null; + shift: ShiftEntity | null; isOpen: boolean; onClose: () => void; } @@ -24,5 +24,5 @@ export interface ShiftDetailModalProps { export interface ShiftCreateModalProps { isOpen: boolean; onClose: () => void; - defaultDate?: string; + defaultDate?: Date; } diff --git a/components/organisms/shift-schedule/WeeklySchedule.tsx b/components/organisms/shift-schedule/WeeklySchedule.tsx index 17c5cb6..487eca3 100644 --- a/components/organisms/shift-schedule/WeeklySchedule.tsx +++ b/components/organisms/shift-schedule/WeeklySchedule.tsx @@ -37,7 +37,7 @@ function formatDateISO(d: Date): string { } function isToday(d: Date): boolean { - const today = new Date(2026, 3, 10); + const today = new Date(Date.now()); return ( d.getDate() === today.getDate() && d.getMonth() === today.getMonth() && @@ -59,36 +59,32 @@ export default function WeeklySchedule({ } = useShift(); const weekDates = getWeekDates(); - const [selectedDate, setSelectedDate] = useState( - formatDateISO(weekDates[0] ?? currentDate), + const [selectedDate, setSelectedDate] = useState( + weekDates[0] ?? currentDate, ); const statusDotsByDate = useMemo(() => { const map: Record = {}; weekDates.forEach((date) => { - const dateStr = formatDateISO(date); - const dayShifts = getShiftsForDate(dateStr); const dots: string[] = []; - if (dayShifts.some((s) => s.status === "available")) - dots.push("bg-sky-300"); - if (dayShifts.some((s) => s.status === "registered")) - dots.push("bg-blue-600"); - if (dayShifts.some((s) => s.status === "approved_leave")) - dots.push("bg-purple-400"); - if (dayShifts.some((s) => s.status === "absent")) - dots.push("bg-rose-400"); - map[dateStr] = dots.slice(0, 3); + // if (dayShifts.some((s) => s.status === "available")) + // dots.push("bg-sky-300"); + // if (dayShifts.some((s) => s.status === "registered")) + // dots.push("bg-blue-600"); + // if (dayShifts.some((s) => s.status === "approved_leave")) + // dots.push("bg-purple-400"); + // if (dayShifts.some((s) => s.status === "absent")) + // dots.push("bg-rose-400"); + map[date.toISOString()] = dots.slice(0, 3); }); return map; }, [weekDates, getShiftsForDate]); - const selectedDateObj = useMemo(() => { - const inWeek = weekDates.find((d) => formatDateISO(d) === selectedDate); + const selectedDateStr = useMemo(() => { + const inWeek = weekDates.find((d) => d === selectedDate); return inWeek ?? weekDates[0] ?? currentDate; }, [selectedDate, weekDates, currentDate]); - const selectedDateStr = formatDateISO(selectedDateObj); - const renderMobileDayView = mobileCalendarHeader && ( @@ -116,14 +112,13 @@ export default function WeeklySchedule({ {weekDates.map((date, i) => { - const dateStr = formatDateISO(date); - const active = dateStr === selectedDateStr; - const dots = statusDotsByDate[dateStr] ?? []; + const active = date === selectedDateStr; + const dots = statusDotsByDate[date.toISOString()] ?? []; return ( setSelectedDate(dateStr)} + onClick={() => setSelectedDate(date)} className={`flex cursor-pointer flex-col items-center gap-1 rounded-xl border-none p-1 ${ active ? "bg-(--color-primary)/10" : "bg-transparent" }`} @@ -135,14 +130,14 @@ export default function WeeklySchedule({ = 5 ? "text-pink-500" : "text-sky-500" }`} > - {date.getDate()} + {new Date(date).getDate()} {dots.map((dot, idx) => ( @@ -160,9 +155,10 @@ export default function WeeklySchedule({ {DEPARTMENTS.map((dept) => { - const deptShifts = getShiftsForDate(selectedDateStr).filter( - (s) => s.department === dept.id, - ); + const deptShifts = getShiftsForDate(new Date(selectedDateStr)); + // .filter( + // (s) => s.department === dept.id, + // ); if (deptShifts.length === 0 && !onCreateShift) return null; return ( {DAY_LABELS[i]} - {formatDateShort(date)} + {date.toISOString()} ))} @@ -246,15 +242,16 @@ export default function WeeklySchedule({ {weekDates.map((date, i) => { - const dateStr = formatDateISO(date); - const shifts = getShiftsForDate(dateStr).filter( - (s) => s.department === dept.id, - ); + const dateStr = date; + const shifts = getShiftsForDate(new Date(date)); + // .filter( + // (s) => s.department === dept.id, + // ); return ( diff --git a/components/organisms/shop-grid/ShopGrid.tsx b/components/organisms/shop-grid/ShopGrid.tsx index c37d88d..143edd4 100644 --- a/components/organisms/shop-grid/ShopGrid.tsx +++ b/components/organisms/shop-grid/ShopGrid.tsx @@ -1,46 +1,46 @@ "use client"; -import { ShopCard } from "@/components/molecules/cards"; -import { MOCK_SHOPS } from "@/lib/constants"; +// import { ShopCard } from "@/components/molecules/cards"; +// import { MOCK_SHOPS } from "@/lib/constants"; -import type { ShopGridProps } from "./ShopGrid.types"; +// import type { ShopGridProps } from "./ShopGrid.types"; -export default function ShopGrid({ - searchName = "", - searchAddress = "", -}: ShopGridProps) { - const filtered = MOCK_SHOPS.filter((shop) => { - const matchesName = - searchName.trim() === "" || - shop.name.toLowerCase().includes(searchName.toLowerCase()); - const matchesAddress = - searchAddress.trim() === "" || - shop.address.toLowerCase().includes(searchAddress.toLowerCase()); - return matchesName && matchesAddress; - }); +// export default function ShopGrid({ +// searchName = "", +// searchAddress = "", +// }: ShopGridProps) { +// const filtered = MOCK_SHOPS.filter((shop) => { +// const matchesName = +// searchName.trim() === "" || +// shop.name.toLowerCase().includes(searchName.toLowerCase()); +// const matchesAddress = +// searchAddress.trim() === "" || +// shop.address.toLowerCase().includes(searchAddress.toLowerCase()); +// return matchesName && matchesAddress; +// }); - if (filtered.length === 0) { - return ( - - - - No shops found matching your search - - - ); - } +// if (filtered.length === 0) { +// return ( +// +// +// +// No shops found matching your search +// +// +// ); +// } - return ( - - {filtered.map((shop) => ( - - ))} - - ); -} +// return ( +// +// {filtered.map((shop) => ( +// +// ))} +// +// ); +// } diff --git a/components/organisms/shop-grid/index.ts b/components/organisms/shop-grid/index.ts index 481eebf..1359cdc 100644 --- a/components/organisms/shop-grid/index.ts +++ b/components/organisms/shop-grid/index.ts @@ -1,2 +1,2 @@ -export { default as ShopGrid } from "./ShopGrid"; +// export { default as ShopGrid } from "./ShopGrid"; export type { ShopGridProps } from "./ShopGrid.types"; diff --git a/lib/constants.ts b/lib/constants.ts index d12dd53..26e963c 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -1,13 +1,9 @@ import type { Department, - MenuItemEntity, - ProductSalesStats, RevenueDataPoint, - ShiftSlot, - Shop, + ShiftEntity, ShopInfo, SocialLinks, - User, } from "./types"; // ===== SHOP INFORMATION ===== @@ -33,45 +29,6 @@ export const SOCIAL_LINKS: SocialLinks = { website: "/", }; -// ===== MOCK SHOPS (for Feed page) ===== -export const MOCK_SHOPS: Shop[] = [ - { - id: 1, - name: "The Coffee House", - address: "86 Cao Thắng, Quận 3, TP. Hồ Chí Minh", - image: - "https://images.unsplash.com/photo-1554118811-1e0d58224f24?w=600&h=400&fit=crop", - }, - { - id: 2, - name: "Highlands Coffee", - address: "123 Nguyễn Huệ, Quận 1, TP. Hồ Chí Minh", - image: - "https://images.unsplash.com/photo-1559305616-3f99cd43e353?w=600&h=400&fit=crop", - }, - { - id: 3, - name: "Phúc Long Heritage", - address: "42 Lê Lợi, Quận 1, TP. Hồ Chí Minh", - image: - "https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?w=600&h=400&fit=crop", - }, - { - id: 4, - name: "Katinat Saigon Kafe", - address: "26 Lý Tự Trọng, Quận 1, TP. Hồ Chí Minh", - image: - "https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?w=600&h=400&fit=crop", - }, - { - id: 5, - name: "Trung Nguyên E-Coffee", - address: "15 Hai Bà Trưng, Quận 1, TP. Hồ Chí Minh", - image: - "https://images.unsplash.com/photo-1453614512568-c4024d13c247?w=600&h=400&fit=crop", - }, -]; - // ===== MOCK FINANCIAL DATA ===== // Daily revenue for the last 30 days (current month) @@ -153,9 +110,9 @@ export const MOCK_REVENUE_YEARLY: RevenueDataPoint[] = [ * Generate mock shift slots for the weeks around today (April 2026). * Covers Mon 6 Apr – Sun 26 Apr 2026. */ -function generateMockShifts(): ShiftSlot[] { - const shifts: ShiftSlot[] = []; - const departments = ["bar", "kitchen", "cashier", "janitor"]; +function generateMockShifts(): ShiftEntity[] { + const shifts: ShiftEntity[] = []; + const departments = ["waiter"]; const timeSlots = [ { start: "07:00", end: "11:00", hours: 4, wage: 120000 }, { start: "11:00", end: "15:00", hours: 4, wage: 120000 }, @@ -189,7 +146,7 @@ function generateMockShifts(): ShiftSlot[] { // Determine registration status const registered: { id: number; name: string }[] = []; - let status: ShiftSlot["status"] = "available"; + // let status: ShiftEntity["status"] = "available"; // Past shifts (before April 10) are mostly registered if (day < 10) { @@ -198,37 +155,37 @@ function generateMockShifts(): ShiftSlot[] { if (shiftCounter % 7 === 0) { registered.push(staffPool[(staffIdx + 1) % staffPool.length]); } - status = "registered"; + // status = "registered"; // Some past shifts have leave/absent - if (shiftCounter % 11 === 0) status = "approved_leave"; - if (shiftCounter % 13 === 0) status = "absent"; + // if (shiftCounter % 11 === 0) status = "approved_leave"; + // if (shiftCounter % 13 === 0) status = "absent"; } // Current week (April 6-12): mix of registered and available else if (day >= 10 && day <= 12) { if (shiftCounter % 3 === 0) { registered.push(staffPool[shiftCounter % staffPool.length]); - status = "registered"; + // status = "registered"; } } // Future shifts: mostly available, some registered else { if (shiftCounter % 5 === 0) { registered.push(staffPool[shiftCounter % staffPool.length]); - status = "registered"; + // status = "registered"; } } shifts.push({ id: "shiftId", - date: dateStr, + date: new Date(dateStr), startTime: slot.start, endTime: slot.end, - durationHours: slot.hours, + // durationHours: slot.hours, wage: slot.wage, - department: dept, + // department: dept, maxStaff: dept === "bar" ? 3 : 2, - registeredStaff: registered, - status, + // registeredStaff: registered, + // status, }); } } @@ -237,11 +194,8 @@ function generateMockShifts(): ShiftSlot[] { return shifts; } -export const MOCK_SHIFT_SLOTS: ShiftSlot[] = generateMockShifts(); +export const MOCK_SHIFT_SLOTS: ShiftEntity[] = generateMockShifts(); export const DEPARTMENTS: Department[] = [ - { id: "bar", name: "Bar Staff", icon: "fa-solid fa-martini-glass-citrus" }, - { id: "kitchen", name: "Kitchen", icon: "fa-solid fa-kitchen-set" }, - { id: "cashier", name: "Cashier", icon: "fa-solid fa-cash-register" }, - { id: "janitor", name: "Janitor", icon: "fa-solid fa-broom" }, -]; \ No newline at end of file + { id: "waiter", name: "Bar Staff", icon: "fa-brands fa-jenkins" }, +]; diff --git a/lib/shift-context.tsx b/lib/shift-context.tsx index 485e481..b28542e 100644 --- a/lib/shift-context.tsx +++ b/lib/shift-context.tsx @@ -1,15 +1,22 @@ "use client"; +import { gql } from "@apollo/client"; +import { useMutation, useQuery } from "@apollo/client/react"; import { ReactNode, createContext, useCallback, useContext, + useEffect, useState, } from "react"; -import { MOCK_SHIFT_SLOTS } from "./constants"; -import type { ShiftSlot, ShiftStatus } from "./types"; +import { eateryClient } from "./apollo-clients"; +import { + type ShiftEntity, + type allShiftsQuery, + createShiftMutation, +} from "./types"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -17,7 +24,7 @@ export type ScheduleView = "week" | "month"; interface ShiftContextType { // Data - shifts: ShiftSlot[]; + shifts: ShiftEntity[]; view: ScheduleView; setView: (view: ScheduleView) => void; @@ -32,23 +39,23 @@ interface ShiftContextType { // Shift actions registerShift: ( shiftId: string, - staffId: number, + staffId: string, staffName: string, ) => { success: boolean; error?: string }; - unregisterShift: (shiftId: string, staffId: number) => void; - createShift: (shift: Omit) => void; - updateShift: (shift: ShiftSlot) => void; + unregisterShift: (shiftId: string, staffId: string) => void; + createShift: (shift: Omit) => void; + updateShift: (shift: ShiftEntity) => void; deleteShift: (shiftId: string) => void; // Helpers - getShiftsForDate: (date: string) => ShiftSlot[]; - getShiftsForWeek: (weekStart: Date) => ShiftSlot[]; + getShiftsForDate: (date: Date) => ShiftEntity[]; + getShiftsForWeek: (weekStart: Date) => ShiftEntity[]; getWeekDates: () => Date[]; hasConflict: ( - date: string, + date: Date, startTime: string, endTime: string, - staffId: number, + staffId: string, excludeShiftId?: string, ) => boolean; getWeeklyBudget: () => number; @@ -94,13 +101,60 @@ function timesOverlap( return s1 < e2 && s2 < e1; } +// ___ GraphQL __________________________________________________________________ + +const GET_ALL_SHIFTS = gql` + query allShifts { + allShifts { + id + date + startTime + endTime + maxStaff + wage + registeredStaff { + staffId + } + } + } +`; + +const CREATE_SHIFT = gql` + mutation createShift($shiftInput: CreateShiftInput!) { + createShift(shiftInput: $shiftInput) { + id + date + startTime + endTime + maxStaff + wage + registeredStaff { + staffId + } + } + } +`; + // ─── Provider ───────────────────────────────────────────────────────────────── export function ShiftProvider({ children }: { children: ReactNode }) { - const [shifts, setShifts] = useState(MOCK_SHIFT_SLOTS); + const [shifts, setShifts] = useState([]); const [view, setView] = useState("week"); const [currentDate, setCurrentDate] = useState(new Date(2026, 3, 10)); // April 10, 2026 + const { data } = useQuery(GET_ALL_SHIFTS, { + client: eateryClient, + fetchPolicy: "network-only", + }); + + const [mutateCreateShift] = useMutation(CREATE_SHIFT, { + client: eateryClient, + }); + + useEffect(() => { + if (data) setShifts(data.allShifts); + }, [data]); + // ── Navigation ────────────────────────────────────────────────────────── const goToNextWeek = useCallback(() => { @@ -151,18 +205,20 @@ export function ShiftProvider({ children }: { children: ReactNode }) { }, [currentDate]); const getShiftsForDate = useCallback( - (date: string): ShiftSlot[] => { - return shifts.filter((s) => s.date === date); + (date: Date): ShiftEntity[] => { + return shifts.filter((s) => { + return new Date(s.date).getDate() === date.getDate(); + }); }, [shifts], ); const getShiftsForWeek = useCallback( - (weekStart: Date): ShiftSlot[] => { + (weekStart: Date): ShiftEntity[] => { const dates = Array.from({ length: 7 }, (_, i) => { const d = new Date(weekStart); d.setDate(weekStart.getDate() + i); - return formatDate(d); + return d; }); return shifts.filter((s) => dates.includes(s.date)); }, @@ -171,17 +227,17 @@ export function ShiftProvider({ children }: { children: ReactNode }) { const hasConflict = useCallback( ( - date: string, + date: Date, startTime: string, endTime: string, - staffId: number, + staffId: string, excludeShiftId?: string, ): boolean => { return shifts.some( (s) => s.date === date && s.id !== excludeShiftId && - s.registeredStaff.some((rs) => rs.id === staffId) && + s.registeredStaff!.some((rs) => rs.id === staffId) && timesOverlap(startTime, endTime, s.startTime, s.endTime), ); }, @@ -189,10 +245,12 @@ export function ShiftProvider({ children }: { children: ReactNode }) { ); const getWeeklyBudget = useCallback((): number => { - const weekDates = getWeekDates().map(formatDate); + const weekDates = getWeekDates(); return shifts - .filter((s) => weekDates.includes(s.date) && s.registeredStaff.length > 0) - .reduce((sum, s) => sum + s.wage * s.registeredStaff.length, 0); + .filter( + (s) => weekDates.includes(s.date) && s.registeredStaff!.length > 0, + ) + .reduce((sum, s) => sum + s.wage * s.registeredStaff!.length, 0); }, [shifts, getWeekDates]); // ── Shift actions ─────────────────────────────────────────────────────── @@ -200,17 +258,17 @@ export function ShiftProvider({ children }: { children: ReactNode }) { const registerShift = useCallback( ( shiftId: string, - staffId: number, + staffId: string, staffName: string, ): { success: boolean; error?: string } => { const shift = shifts.find((s) => s.id === shiftId); if (!shift) return { success: false, error: "Ca làm không tồn tại." }; - if (shift.registeredStaff.length >= shift.maxStaff) { + if (shift.registeredStaff!.length >= shift.maxStaff) { return { success: false, error: "Ca làm đã đủ người." }; } - if (shift.registeredStaff.some((rs) => rs.id === staffId)) { + if (shift.registeredStaff!.some((rs) => rs.id === staffId)) { return { success: false, error: "Bạn đã đăng ký ca này rồi." }; } @@ -229,50 +287,50 @@ export function ShiftProvider({ children }: { children: ReactNode }) { }; } - setShifts((prev) => - prev.map((s) => - s.id === shiftId - ? { - ...s, - registeredStaff: [ - ...s.registeredStaff, - { id: staffId, name: staffName }, - ], - status: "registered" as ShiftStatus, - } - : s, - ), - ); + // setShifts((prev) => + // prev.map((s) => + // s.id === shiftId + // ? { + // ...s, + // registeredStaff: [ + // ...s.registeredStaff, + // { staffId, name: staffName }, + // ], + // status: "registered" as ShiftStatus, + // } + // : s, + // ), + // ); return { success: true }; }, [shifts, hasConflict], ); - const unregisterShift = useCallback((shiftId: string, staffId: number) => { + const unregisterShift = useCallback((shiftId: string, staffId: string) => { setShifts((prev) => prev.map((s) => { if (s.id !== shiftId) return s; - const updated = s.registeredStaff.filter((rs) => rs.id !== staffId); + const updated = s.registeredStaff!.filter((rs) => rs.id !== staffId); return { ...s, registeredStaff: updated, - status: - updated.length === 0 ? ("available" as ShiftStatus) : s.status, }; }), ); }, []); - const createShift = useCallback((shift: Omit) => { - const newShift: ShiftSlot = { - ...shift, - id: `shift_${Date.now()}`, - }; - setShifts((prev) => [...prev, newShift]); + const createShift = useCallback(async (shift: Omit) => { + const { data } = await mutateCreateShift({ + variables: { + shiftInput: shift, + }, + }); + + if (data) setShifts((prev) => [...prev, data.createShift]); }, []); - const updateShift = useCallback((shift: ShiftSlot) => { + const updateShift = useCallback((shift: ShiftEntity) => { setShifts((prev) => prev.map((s) => (s.id === shift.id ? shift : s))); }, []); diff --git a/lib/types.ts b/lib/types.ts index 1730227..08c93b5 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -2,7 +2,7 @@ export type UserRole = "manager" | "staff" | "customer"; export interface User { - id: number; + id: string; name: string; role: UserRole; avatar: string | null; @@ -88,28 +88,26 @@ export type ShiftStatus = | "approved_leave" | "absent"; -export interface RegisteredStaff { - id: number; - name: string; +export interface ShiftRegistrationEntity { + id: string; + staffId: string; + shift: ShiftEntity; } -export interface ShiftSlot { +export interface ShiftEntity { id: string; - date: string; // ISO date string "YYYY-MM-DD" - startTime: string; // "HH:mm" - endTime: string; // "HH:mm" - durationHours: number; - wage: number; // VND per shift - department: string; + date: Date; + startTime: string; + endTime: string; + wage: number; maxStaff: number; - registeredStaff: RegisteredStaff[]; - status: ShiftStatus; + registeredStaff?: ShiftRegistrationEntity[]; } export interface Department { id: string; name: string; - icon: string; // FontAwesome class + icon: string; } export interface MenuItemEntity { @@ -122,8 +120,6 @@ export interface MenuItemEntity { description: string; } -export interface ShiftEntity {} - export interface EateryEntity { id: string; ownerId: string; @@ -172,3 +168,15 @@ export interface createCartMutation { export interface addMenuItemMutation { addItem: CartEntity; } + +export interface allRegistrationsQuery { + allRegistrations: ShiftRegistrationEntity[]; +} + +export interface allShiftsQuery { + allShifts: ShiftEntity[]; +} + +export interface createShiftMutation { + createShift: ShiftEntity; +}
- Find and choose your favorite shop -
{shift.startTime} – {shift.endTime}
- {shift.durationHours}h · {formatWage(shift.wage)} + {}h · {formatWage(shift.wage)}
- {shift.durationHours}h · {formatWage(shift.wage)} VND + {}h · {formatWage(shift.wage)} VND
Nhân viên ({shift.registeredStaff.length}/{shift.maxStaff}) @@ -102,18 +77,18 @@ export default function ShiftCard({ key={s.id} className="rounded-full bg-white/60 px-2 py-0.5 text-[10px] font-medium" > - {s.name} + {s.staffId} ))}
{shift.maxStaff} vị trí còn trống
{dept?.name}
- {shift.durationHours} giờ + {""} giờ
- Nhân viên đã đăng ký ({shift.registeredStaff.length}/ + Nhân viên đã đăng ký ({shift.registeredStaff!.length}/ {shift.maxStaff})
Chưa có ai đăng ký
- No shops found matching your search -
+// No shops found matching your search +//