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 && ( -
- -
- )} -
- - {/* 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 && ( - - )} -
-
-
-
-
- ); -} 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..485b7d3 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,14 +52,14 @@ 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 isManager = user?.role === "manager"; - const handleShiftClick = (shift: ShiftSlot) => { + const handleShiftClick = (shift: ShiftEntity) => { setSelectedShift(shift); setDetailOpen(true); }; diff --git a/components/molecules/cards/ShiftCard.tsx b/components/molecules/cards/ShiftCard.tsx index 479f726..b34e551 100644 --- a/components/molecules/cards/ShiftCard.tsx +++ b/components/molecules/cards/ShiftCard.tsx @@ -1,11 +1,11 @@ "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"], + ShiftEntity["status"], { bg: string; text: string; label: string } > = { available: { 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/shift-schedule/ShiftCreateModal.tsx b/components/organisms/shift-schedule/ShiftCreateModal.tsx index 9a0dddb..420841c 100644 --- a/components/organisms/shift-schedule/ShiftCreateModal.tsx +++ b/components/organisms/shift-schedule/ShiftCreateModal.tsx @@ -49,18 +49,12 @@ export default function ShiftCreateModal({ return; } - const durationHours = (endMinutes - startMinutes) / 60; - createShift({ - date, + date: new Date(date), startTime, endTime, - durationHours, wage, - department, maxStaff, - registeredStaff: [], - status: "available", }); onClose(); diff --git a/components/organisms/shift-schedule/ShiftSchedule.types.ts b/components/organisms/shift-schedule/ShiftSchedule.types.ts index 59dff4e..4e89b32 100644 --- a/components/organisms/shift-schedule/ShiftSchedule.types.ts +++ b/components/organisms/shift-schedule/ShiftSchedule.types.ts @@ -1,22 +1,22 @@ -import type { ShiftSlot } from "@/lib/types"; +import type { ShiftEntity } from "@/lib/types"; export interface WeeklyScheduleProps { - onShiftClick: (shift: ShiftSlot) => void; + onShiftClick: (shift: ShiftEntity) => void; onCreateShift?: (date: string) => void; mobileCalendarHeader?: boolean; } export interface MonthlyCalendarProps { - onShiftClick: (shift: ShiftSlot) => void; + onShiftClick: (shift: ShiftEntity) => void; onDateSelect?: (date: string) => 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; } diff --git a/lib/constants.ts b/lib/constants.ts index d12dd53..10716e3 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) { @@ -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..10b457b 100644 --- a/lib/shift-context.tsx +++ b/lib/shift-context.tsx @@ -1,15 +1,24 @@ "use client"; +import { gql } from "@apollo/client"; +import { useMutation, useQuery } from "@apollo/client/react"; import { ReactNode, createContext, useCallback, useContext, + useEffect, useState, } from "react"; +import { eateryClient } from "./apollo-clients"; import { MOCK_SHIFT_SLOTS } from "./constants"; -import type { ShiftSlot, ShiftStatus } from "./types"; +import { + type ShiftEntity, + type ShiftStatus, + type allShiftsQuery, + createShiftMutation, +} from "./types"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -17,7 +26,7 @@ export type ScheduleView = "week" | "month"; interface ShiftContextType { // Data - shifts: ShiftSlot[]; + shifts: ShiftEntity[]; view: ScheduleView; setView: (view: ScheduleView) => void; @@ -32,23 +41,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 +103,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 +207,18 @@ export function ShiftProvider({ children }: { children: ReactNode }) { }, [currentDate]); const getShiftsForDate = useCallback( - (date: string): ShiftSlot[] => { + (date: Date): ShiftEntity[] => { return shifts.filter((s) => s.date === date); }, [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,10 +227,10 @@ 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( @@ -189,7 +245,7 @@ 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); @@ -200,7 +256,7 @@ 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); @@ -229,27 +285,27 @@ 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; @@ -257,22 +313,22 @@ export function ShiftProvider({ children }: { children: ReactNode }) { 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..2620a43 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -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 { @@ -172,3 +170,15 @@ export interface createCartMutation { export interface addMenuItemMutation { addItem: CartEntity; } + +export interface allRegistrationsQuery { + allRegistrations: ShiftRegistrationEntity[]; +} + +export interface allShiftsQuery { + allShifts: ShiftEntity[]; +} + +export interface createShiftMutation { + createShift: ShiftEntity; +} \ No newline at end of file