chore: update

This commit is contained in:
TakahashiNg
2026-05-14 01:36:15 +00:00
parent dfcb1b09c0
commit 0c7818a974
10 changed files with 143 additions and 224 deletions
-86
View File
@@ -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 (
<main className="bg-background min-h-[calc(100vh-var(--spacing-header-height))]">
<div className="mx-auto max-w-7xl px-4 py-8 md:px-6 lg:px-8">
{/* Page title */}
<div className="mb-8">
<h1 className="text-foreground text-2xl font-bold md:text-3xl">
Explore Shops
</h1>
<p className="mt-1 text-sm text-(--color-text-muted)">
Find and choose your favorite shop
</p>
</div>
{/* Shop grid */}
<div className="mb-10">
<ShopGrid searchName={searchName} searchAddress={searchAddress} />
{hasFilters && (
<div className="mt-4 flex justify-center">
<button
onClick={() => {
setSearchName("");
setSearchAddress("");
}}
className="cursor-pointer border-none bg-transparent text-sm text-(--color-primary) hover:underline"
>
Clear filters
</button>
</div>
)}
</div>
{/* Filter / Search bar — sticky bottom */}
<div className="sticky bottom-0 rounded-2xl border border-(--color-border) bg-(--color-bg-card) p-4 shadow-[0_-2px_16px_var(--color-shadow-sm)] md:p-5">
<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>Filter shops</span>
</div>
{/* Name search */}
<SearchBar
value={searchName}
onChange={setSearchName}
onClear={() => setSearchName("")}
placeholder="Search by shop name..."
className="min-w-0 flex-1"
/>
{/* Address search — different icon so not using SearchBar atom */}
<div className="relative min-w-0 flex-1">
<i className="fa-solid fa-location-dot pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-xs text-(--color-text-muted)"></i>
<input
type="text"
value={searchAddress}
onChange={(e) => 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 && (
<button
onClick={() => 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)"
>
<i className="fa-solid fa-xmark text-sm"></i>
</button>
)}
</div>
</div>
</div>
</div>
</main>
);
}
-9
View File
@@ -1,9 +0,0 @@
import { FeedLayout } from "@/components/templates/feed-layout";
export default function RootFeedLayout({
children,
}: {
children: React.ReactNode;
}) {
return <FeedLayout>{children}</FeedLayout>;
}
+3 -3
View File
@@ -7,7 +7,7 @@ import ShiftDetailModal from "@/components/organisms/shift-schedule/ShiftDetailM
import WeeklySchedule from "@/components/organisms/shift-schedule/WeeklySchedule"; import WeeklySchedule from "@/components/organisms/shift-schedule/WeeklySchedule";
import { useAuth } from "@/lib/auth-context"; import { useAuth } from "@/lib/auth-context";
import { useShift } from "@/lib/shift-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 Link from "next/link";
import { useState } from "react"; import { useState } from "react";
@@ -52,14 +52,14 @@ export default function StaffSchedulePage() {
getWeeklyBudget, getWeeklyBudget,
} = useShift(); } = useShift();
const [selectedShift, setSelectedShift] = useState<ShiftSlot | null>(null); const [selectedShift, setSelectedShift] = useState<ShiftEntity | null>(null);
const [detailOpen, setDetailOpen] = useState(false); const [detailOpen, setDetailOpen] = useState(false);
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [createDate, setCreateDate] = useState<string | undefined>(); const [createDate, setCreateDate] = useState<string | undefined>();
const isManager = user?.role === "manager"; const isManager = user?.role === "manager";
const handleShiftClick = (shift: ShiftSlot) => { const handleShiftClick = (shift: ShiftEntity) => {
setSelectedShift(shift); setSelectedShift(shift);
setDetailOpen(true); setDetailOpen(true);
}; };
+2 -2
View File
@@ -1,11 +1,11 @@
"use client"; "use client";
import type { ShiftSlot } from "@/lib/types"; import type { ShiftEntity } from "@/lib/types";
import type { ShiftCardProps } from "./ShiftCard.types"; import type { ShiftCardProps } from "./ShiftCard.types";
const STATUS_STYLES: Record< const STATUS_STYLES: Record<
ShiftSlot["status"], ShiftEntity["status"],
{ bg: string; text: string; label: string } { bg: string; text: string; label: string }
> = { > = {
available: { available: {
@@ -1,7 +1,7 @@
import type { ShiftSlot } from "@/lib/types"; import type { ShiftEntity } from "@/lib/types";
export interface ShiftCardProps { export interface ShiftCardProps {
shift: ShiftSlot; shift: ShiftEntity;
compact?: boolean; compact?: boolean;
onClick?: (shift: ShiftSlot) => void; onClick?: (shift: ShiftEntity) => void;
} }
@@ -49,18 +49,12 @@ export default function ShiftCreateModal({
return; return;
} }
const durationHours = (endMinutes - startMinutes) / 60;
createShift({ createShift({
date, date: new Date(date),
startTime, startTime,
endTime, endTime,
durationHours,
wage, wage,
department,
maxStaff, maxStaff,
registeredStaff: [],
status: "available",
}); });
onClose(); onClose();
@@ -1,22 +1,22 @@
import type { ShiftSlot } from "@/lib/types"; import type { ShiftEntity } from "@/lib/types";
export interface WeeklyScheduleProps { export interface WeeklyScheduleProps {
onShiftClick: (shift: ShiftSlot) => void; onShiftClick: (shift: ShiftEntity) => void;
onCreateShift?: (date: string) => void; onCreateShift?: (date: string) => void;
mobileCalendarHeader?: boolean; mobileCalendarHeader?: boolean;
} }
export interface MonthlyCalendarProps { export interface MonthlyCalendarProps {
onShiftClick: (shift: ShiftSlot) => void; onShiftClick: (shift: ShiftEntity) => void;
onDateSelect?: (date: string) => void; onDateSelect?: (date: string) => void;
} }
export interface MobileShiftViewProps { export interface MobileShiftViewProps {
onShiftClick: (shift: ShiftSlot) => void; onShiftClick: (shift: ShiftEntity) => void;
} }
export interface ShiftDetailModalProps { export interface ShiftDetailModalProps {
shift: ShiftSlot | null; shift: ShiftEntity | null;
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
} }
+7 -53
View File
@@ -1,13 +1,9 @@
import type { import type {
Department, Department,
MenuItemEntity,
ProductSalesStats,
RevenueDataPoint, RevenueDataPoint,
ShiftSlot, ShiftEntity,
Shop,
ShopInfo, ShopInfo,
SocialLinks, SocialLinks,
User,
} from "./types"; } from "./types";
// ===== SHOP INFORMATION ===== // ===== SHOP INFORMATION =====
@@ -33,45 +29,6 @@ export const SOCIAL_LINKS: SocialLinks = {
website: "/", 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 ===== // ===== MOCK FINANCIAL DATA =====
// Daily revenue for the last 30 days (current month) // 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). * Generate mock shift slots for the weeks around today (April 2026).
* Covers Mon 6 Apr Sun 26 Apr 2026. * Covers Mon 6 Apr Sun 26 Apr 2026.
*/ */
function generateMockShifts(): ShiftSlot[] { function generateMockShifts(): ShiftEntity[] {
const shifts: ShiftSlot[] = []; const shifts: ShiftEntity[] = [];
const departments = ["bar", "kitchen", "cashier", "janitor"]; const departments = ["waiter"];
const timeSlots = [ const timeSlots = [
{ start: "07:00", end: "11:00", hours: 4, wage: 120000 }, { start: "07:00", end: "11:00", hours: 4, wage: 120000 },
{ start: "11:00", end: "15: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 // Determine registration status
const registered: { id: number; name: string }[] = []; const registered: { id: number; name: string }[] = [];
let status: ShiftSlot["status"] = "available"; let status: ShiftEntity["status"] = "available";
// Past shifts (before April 10) are mostly registered // Past shifts (before April 10) are mostly registered
if (day < 10) { if (day < 10) {
@@ -237,11 +194,8 @@ function generateMockShifts(): ShiftSlot[] {
return shifts; return shifts;
} }
export const MOCK_SHIFT_SLOTS: ShiftSlot[] = generateMockShifts(); export const MOCK_SHIFT_SLOTS: ShiftEntity[] = generateMockShifts();
export const DEPARTMENTS: Department[] = [ export const DEPARTMENTS: Department[] = [
{ id: "bar", name: "Bar Staff", icon: "fa-solid fa-martini-glass-citrus" }, { id: "waiter", name: "Bar Staff", icon: "fa-brands fa-jenkins" },
{ 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" },
]; ];
+98 -42
View File
@@ -1,15 +1,24 @@
"use client"; "use client";
import { gql } from "@apollo/client";
import { useMutation, useQuery } from "@apollo/client/react";
import { import {
ReactNode, ReactNode,
createContext, createContext,
useCallback, useCallback,
useContext, useContext,
useEffect,
useState, useState,
} from "react"; } from "react";
import { eateryClient } from "./apollo-clients";
import { MOCK_SHIFT_SLOTS } from "./constants"; import { MOCK_SHIFT_SLOTS } from "./constants";
import type { ShiftSlot, ShiftStatus } from "./types"; import {
type ShiftEntity,
type ShiftStatus,
type allShiftsQuery,
createShiftMutation,
} from "./types";
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
@@ -17,7 +26,7 @@ export type ScheduleView = "week" | "month";
interface ShiftContextType { interface ShiftContextType {
// Data // Data
shifts: ShiftSlot[]; shifts: ShiftEntity[];
view: ScheduleView; view: ScheduleView;
setView: (view: ScheduleView) => void; setView: (view: ScheduleView) => void;
@@ -32,23 +41,23 @@ interface ShiftContextType {
// Shift actions // Shift actions
registerShift: ( registerShift: (
shiftId: string, shiftId: string,
staffId: number, staffId: string,
staffName: string, staffName: string,
) => { success: boolean; error?: string }; ) => { success: boolean; error?: string };
unregisterShift: (shiftId: string, staffId: number) => void; unregisterShift: (shiftId: string, staffId: string) => void;
createShift: (shift: Omit<ShiftSlot, "id">) => void; createShift: (shift: Omit<ShiftEntity, "id">) => void;
updateShift: (shift: ShiftSlot) => void; updateShift: (shift: ShiftEntity) => void;
deleteShift: (shiftId: string) => void; deleteShift: (shiftId: string) => void;
// Helpers // Helpers
getShiftsForDate: (date: string) => ShiftSlot[]; getShiftsForDate: (date: Date) => ShiftEntity[];
getShiftsForWeek: (weekStart: Date) => ShiftSlot[]; getShiftsForWeek: (weekStart: Date) => ShiftEntity[];
getWeekDates: () => Date[]; getWeekDates: () => Date[];
hasConflict: ( hasConflict: (
date: string, date: Date,
startTime: string, startTime: string,
endTime: string, endTime: string,
staffId: number, staffId: string,
excludeShiftId?: string, excludeShiftId?: string,
) => boolean; ) => boolean;
getWeeklyBudget: () => number; getWeeklyBudget: () => number;
@@ -94,13 +103,60 @@ function timesOverlap(
return s1 < e2 && s2 < e1; 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 ───────────────────────────────────────────────────────────────── // ─── Provider ─────────────────────────────────────────────────────────────────
export function ShiftProvider({ children }: { children: ReactNode }) { export function ShiftProvider({ children }: { children: ReactNode }) {
const [shifts, setShifts] = useState<ShiftSlot[]>(MOCK_SHIFT_SLOTS); const [shifts, setShifts] = useState<ShiftEntity[]>([]);
const [view, setView] = useState<ScheduleView>("week"); const [view, setView] = useState<ScheduleView>("week");
const [currentDate, setCurrentDate] = useState<Date>(new Date(2026, 3, 10)); // April 10, 2026 const [currentDate, setCurrentDate] = useState<Date>(new Date(2026, 3, 10)); // April 10, 2026
const { data } = useQuery<allShiftsQuery>(GET_ALL_SHIFTS, {
client: eateryClient,
fetchPolicy: "network-only",
});
const [mutateCreateShift] = useMutation<createShiftMutation>(CREATE_SHIFT, {
client: eateryClient,
});
useEffect(() => {
if (data) setShifts(data.allShifts);
}, [data]);
// ── Navigation ────────────────────────────────────────────────────────── // ── Navigation ──────────────────────────────────────────────────────────
const goToNextWeek = useCallback(() => { const goToNextWeek = useCallback(() => {
@@ -151,18 +207,18 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
}, [currentDate]); }, [currentDate]);
const getShiftsForDate = useCallback( const getShiftsForDate = useCallback(
(date: string): ShiftSlot[] => { (date: Date): ShiftEntity[] => {
return shifts.filter((s) => s.date === date); return shifts.filter((s) => s.date === date);
}, },
[shifts], [shifts],
); );
const getShiftsForWeek = useCallback( const getShiftsForWeek = useCallback(
(weekStart: Date): ShiftSlot[] => { (weekStart: Date): ShiftEntity[] => {
const dates = Array.from({ length: 7 }, (_, i) => { const dates = Array.from({ length: 7 }, (_, i) => {
const d = new Date(weekStart); const d = new Date(weekStart);
d.setDate(weekStart.getDate() + i); d.setDate(weekStart.getDate() + i);
return formatDate(d); return d;
}); });
return shifts.filter((s) => dates.includes(s.date)); return shifts.filter((s) => dates.includes(s.date));
}, },
@@ -171,10 +227,10 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
const hasConflict = useCallback( const hasConflict = useCallback(
( (
date: string, date: Date,
startTime: string, startTime: string,
endTime: string, endTime: string,
staffId: number, staffId: string,
excludeShiftId?: string, excludeShiftId?: string,
): boolean => { ): boolean => {
return shifts.some( return shifts.some(
@@ -189,7 +245,7 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
); );
const getWeeklyBudget = useCallback((): number => { const getWeeklyBudget = useCallback((): number => {
const weekDates = getWeekDates().map(formatDate); const weekDates = getWeekDates();
return shifts return shifts
.filter((s) => weekDates.includes(s.date) && s.registeredStaff.length > 0) .filter((s) => weekDates.includes(s.date) && s.registeredStaff.length > 0)
.reduce((sum, s) => sum + s.wage * 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( const registerShift = useCallback(
( (
shiftId: string, shiftId: string,
staffId: number, staffId: string,
staffName: string, staffName: string,
): { success: boolean; error?: string } => { ): { success: boolean; error?: string } => {
const shift = shifts.find((s) => s.id === shiftId); const shift = shifts.find((s) => s.id === shiftId);
@@ -229,27 +285,27 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
}; };
} }
setShifts((prev) => // setShifts((prev) =>
prev.map((s) => // prev.map((s) =>
s.id === shiftId // s.id === shiftId
? { // ? {
...s, // ...s,
registeredStaff: [ // registeredStaff: [
...s.registeredStaff, // ...s.registeredStaff,
{ id: staffId, name: staffName }, // { staffId, name: staffName },
], // ],
status: "registered" as ShiftStatus, // status: "registered" as ShiftStatus,
} // }
: s, // : s,
), // ),
); // );
return { success: true }; return { success: true };
}, },
[shifts, hasConflict], [shifts, hasConflict],
); );
const unregisterShift = useCallback((shiftId: string, staffId: number) => { const unregisterShift = useCallback((shiftId: string, staffId: string) => {
setShifts((prev) => setShifts((prev) =>
prev.map((s) => { prev.map((s) => {
if (s.id !== shiftId) return s; if (s.id !== shiftId) return s;
@@ -257,22 +313,22 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
return { return {
...s, ...s,
registeredStaff: updated, registeredStaff: updated,
status:
updated.length === 0 ? ("available" as ShiftStatus) : s.status,
}; };
}), }),
); );
}, []); }, []);
const createShift = useCallback((shift: Omit<ShiftSlot, "id">) => { const createShift = useCallback(async (shift: Omit<ShiftEntity, "id">) => {
const newShift: ShiftSlot = { const { data } = await mutateCreateShift({
...shift, variables: {
id: `shift_${Date.now()}`, shiftInput: shift,
}; },
setShifts((prev) => [...prev, newShift]); });
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))); setShifts((prev) => prev.map((s) => (s.id === shift.id ? shift : s)));
}, []); }, []);
+23 -13
View File
@@ -88,28 +88,26 @@ export type ShiftStatus =
| "approved_leave" | "approved_leave"
| "absent"; | "absent";
export interface RegisteredStaff { export interface ShiftRegistrationEntity {
id: number; id: string;
name: string; staffId: string;
shift: ShiftEntity;
} }
export interface ShiftSlot { export interface ShiftEntity {
id: string; id: string;
date: string; // ISO date string "YYYY-MM-DD" date: Date;
startTime: string; // "HH:mm" startTime: string;
endTime: string; // "HH:mm" endTime: string;
durationHours: number; wage: number;
wage: number; // VND per shift
department: string;
maxStaff: number; maxStaff: number;
registeredStaff: RegisteredStaff[]; registeredStaff?: ShiftRegistrationEntity[];
status: ShiftStatus;
} }
export interface Department { export interface Department {
id: string; id: string;
name: string; name: string;
icon: string; // FontAwesome class icon: string;
} }
export interface MenuItemEntity { export interface MenuItemEntity {
@@ -172,3 +170,15 @@ export interface createCartMutation {
export interface addMenuItemMutation { export interface addMenuItemMutation {
addItem: CartEntity; addItem: CartEntity;
} }
export interface allRegistrationsQuery {
allRegistrations: ShiftRegistrationEntity[];
}
export interface allShiftsQuery {
allShifts: ShiftEntity[];
}
export interface createShiftMutation {
createShift: ShiftEntity;
}