chore: update
This commit is contained in:
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { FeedLayout } from "@/components/templates/feed-layout";
|
|
||||||
|
|
||||||
export default function RootFeedLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return <FeedLayout>{children}</FeedLayout>;
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user