Co-authored-by: TakahashiNg <83152264+TakahashiNguyen@users.noreply.github.com> Reviewed-on: #41
This commit was merged in pull request #41.
This commit is contained in:
+18
-64
@@ -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" },
|
||||
];
|
||||
{ id: "waiter", name: "Bar Staff", icon: "fa-brands fa-jenkins" },
|
||||
];
|
||||
|
||||
+108
-50
@@ -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<ShiftSlot, "id">) => void;
|
||||
updateShift: (shift: ShiftSlot) => void;
|
||||
unregisterShift: (shiftId: string, staffId: string) => void;
|
||||
createShift: (shift: Omit<ShiftEntity, "id">) => 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<ShiftSlot[]>(MOCK_SHIFT_SLOTS);
|
||||
const [shifts, setShifts] = useState<ShiftEntity[]>([]);
|
||||
const [view, setView] = useState<ScheduleView>("week");
|
||||
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 ──────────────────────────────────────────────────────────
|
||||
|
||||
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<ShiftSlot, "id">) => {
|
||||
const newShift: ShiftSlot = {
|
||||
...shift,
|
||||
id: `shift_${Date.now()}`,
|
||||
};
|
||||
setShifts((prev) => [...prev, newShift]);
|
||||
const createShift = useCallback(async (shift: Omit<ShiftEntity, "id">) => {
|
||||
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)));
|
||||
}, []);
|
||||
|
||||
|
||||
+24
-16
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user