Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added api
Binary file not shown.
25 changes: 19 additions & 6 deletions client/web/app/(resa)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,18 @@ function ResaHeader({
<div className="flex flex-1 items-center justify-between px-3">
{/* Left side */}
<div className="flex items-center gap-2">
<SidebarTrigger onClick={() => {
// If opening left sidebar on small screen, close right sidebar
if (!leftSidebarOpen && window.innerWidth < 1024 && setRightSidebarOpen) {
setRightSidebarOpen(false);
}
}} />
<SidebarTrigger
onClick={() => {
// If opening left sidebar on small screen, close right sidebar
if (
!leftSidebarOpen &&
window.innerWidth < 1024 &&
setRightSidebarOpen
) {
setRightSidebarOpen(false);
}
}}
/>
<Separator
orientation="vertical"
className="mr-2 data-[orientation=vertical]:h-4"
Expand All @@ -157,6 +163,13 @@ function ResaHeader({
{/* Right side - Week navigation (conditionally rendered) */}
{weekNav && (
<div className="flex items-center gap-2">
<Button variant="outline" className="h-8">
Week
<ChevronDown className="ml-2 h-4 w-4" />
</Button>
<Button variant="outline" className="h-8">
Today
</Button>
<Button
variant="ghost"
size="icon"
Expand Down
51 changes: 43 additions & 8 deletions client/web/components/calendar/calendar-click-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,57 @@
"use client";
"use client"

import { Plus, Calendar } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Plus, Calendar } from "lucide-react"
import { Button } from "@/components/ui/button"
import { InlineEventForm } from "./inline-event-form"
import type { Employee } from "@/types/employee"

interface CalendarClickMenuProps {
onCreateShift: () => void;
onCreateEvent: () => void;
onCreateShift: () => void
onCreateEvent: () => void
// Props for inline event creation
showEventForm?: boolean
restaurantId?: number | null
date?: string
startTime?: string
endTime?: string
employees?: Employee[]
onEventCreated?: () => void
onEventFormCancel?: () => void
}

/**
* Menu shown when clicking on an empty area of the calendar.
* Provides options to create a shift or event at the clicked position.
* When showEventForm is true, displays the inline event creation form instead.
*/
export function CalendarClickMenu({
onCreateShift,
onCreateEvent,
showEventForm = false,
restaurantId,
date,
startTime,
endTime,
employees = [],
onEventCreated,
onEventFormCancel,
}: CalendarClickMenuProps) {
// If showing inline event form, render it instead of menu
if (showEventForm && restaurantId && date && startTime && endTime) {
return (
<InlineEventForm
restaurantId={restaurantId}
date={date}
startTime={startTime}
endTime={endTime}
employees={employees}
onSuccess={onEventCreated || (() => {})}
onCancel={onEventFormCancel || (() => {})}
/>
)
}

// Default menu view
return (
<div className="flex flex-col">
<Button
Expand All @@ -28,13 +64,12 @@ export function CalendarClickMenu({
</Button>
<Button
variant="ghost"
className="justify-start h-7 opacity-50"
className="justify-start h-7"
onClick={onCreateEvent}
disabled
>
<Calendar className="h-2 w-3" />
Create Event
</Button>
</div>
);
)
}
140 changes: 100 additions & 40 deletions client/web/components/calendar/calendar-grid.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
"use client";

import { useMemo, useState, useRef, useEffect } from "react";
import type { ScheduledShift } from "@/types/schedule";
import type { ShiftTemplate } from "@/types/shift-template";
import type { Employee } from "@/types/employee";
import type { Role } from "@/types/role";
import { DayHeader } from "./day-header";
import { TimeColumn } from "./time-column";
import { TimeSlotCell } from "./time-slot-cell";
import { OverlayLayer } from "./overlay-layer";
import { CalendarClickMenu } from "./calendar-click-menu";
import { CurrentTimeIndicator } from "./current-time-indicator";
import { getAllHours, getWeekDates } from "@/lib/calendar/shift-utils";
import { getTimezoneAbbreviation } from "@/lib/time";
// Note: calculateClickPosition no longer used - using inline calculations instead
import { useRoles } from "@/hooks/use-roles";
import { useRestaurant } from "@/contexts/restaurant-context";
import { useShiftTemplateContext } from "@/contexts/shift-template-context";
"use client"

import { useMemo, useState, useRef, useEffect } from "react"
import type { ScheduledShift } from "@/types/schedule"
import type { ShiftTemplate } from "@/types/shift-template"
import type { Employee } from "@/types/employee"
import type { Role } from "@/types/role"
import { DayHeader } from "./day-header"
import { TimeColumn } from "./time-column"
import { TimeSlotCell } from "./time-slot-cell"
import { OverlayLayer } from "./overlay-layer"
import { CalendarClickMenu } from "./calendar-click-menu"
import { CurrentTimeIndicator } from "./current-time-indicator"
import { getAllHours, getWeekDates } from "@/lib/calendar/shift-utils"
import { getTimezoneAbbreviation } from "@/lib/time"
import { useRoles } from "@/hooks/use-roles"
import { useRestaurant } from "@/contexts/restaurant-context"
import { useShiftTemplateContext } from "@/contexts/shift-template-context"
import { useEvents } from "@/hooks/use-events"
import {
Popover,
PopoverAnchor,
PopoverContent,
} from "@/components/ui/popover";
import { ShiftTemplateFormDialog } from "@/components/schedules/shift-template-form-dialog";
import { EventEditDialog } from "./event-edit-dialog";
import { SelectionHighlight } from "./selection-highlight";
import { hoursToTimeString, roundToQuarterHour } from "@/lib/time";
import type { Event } from "@/types/event"

/** Constants for calendar grid calculations */
const PIXELS_PER_HOUR = 60;
Expand Down Expand Up @@ -90,9 +92,15 @@ export function CalendarGrid({
const hours = getAllHours();

// Fetch roles once at grid level
const { selectedRestaurantId } = useRestaurant();
const { roles, isLoading: rolesLoading } = useRoles(selectedRestaurantId);
const { refetch: refetchShiftTemplates } = useShiftTemplateContext();
const { selectedRestaurantId } = useRestaurant()
const { roles, isLoading: rolesLoading } = useRoles(selectedRestaurantId)
const { refetch: refetchShiftTemplates } = useShiftTemplateContext()

// Fetch events for the visible week
const { events, refetch: refetchEvents } = useEvents(selectedRestaurantId, {
startDate: weekDates[0],
endDate: weekDates[6],
})

// Create role lookup map for O(1) access
const roleMap = useMemo(() => {
Expand All @@ -102,13 +110,18 @@ export function CalendarGrid({
}, [roles]);

// State for click-to-create popover
const [popoverOpen, setPopoverOpen] = useState(false);
const [popoverOpen, setPopoverOpen] = useState(false)
const [clickPosition, setClickPosition] = useState<{
x: number;
y: number;
side: "left" | "right";
} | null>(null);
const [shiftDialogOpen, setShiftDialogOpen] = useState(false);
x: number
y: number
side: "left" | "right"
} | null>(null)
const [shiftDialogOpen, setShiftDialogOpen] = useState(false)
const [showEventForm, setShowEventForm] = useState(false)

// State for event editing
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null)
const [eventDialogOpen, setEventDialogOpen] = useState(false)

// State for time selection (double-click or drag)
const [selection, setSelection] = useState<SelectionState | null>(null);
Expand Down Expand Up @@ -149,16 +162,17 @@ export function CalendarGrid({
const handlePopoverOpenChange = (open: boolean) => {
if (!open) {
// Mark that we just closed the popover
justClosedRef.current = true;
justClosedRef.current = true
// Reset the flag after a short delay
setTimeout(() => {
justClosedRef.current = false;
}, 100);
// Clear selection when popover closes
setSelection(null);
justClosedRef.current = false
}, 100)
// Clear selection and event form when popover closes
setSelection(null)
setShowEventForm(false)
}
setPopoverOpen(open);
};
setPopoverOpen(open)
}

/**
* Convert pixel Y position to time string (HH:MM format)
Expand Down Expand Up @@ -411,10 +425,36 @@ export function CalendarGrid({
setShiftDialogOpen(true);
};

// Handle "Create Event" button click (placeholder)
// Handle "Create Event" button click - show inline event form
const handleCreateEvent = () => {
// No functionality yet
};
setShowEventForm(true)
}

// Handle successful event creation
const handleEventCreated = () => {
setShowEventForm(false)
setPopoverOpen(false)
setSelection(null)
refetchEvents()
}

// Handle event form cancel
const handleEventFormCancel = () => {
setShowEventForm(false)
}

// Handle existing event click
const handleEventClick = (event: Event) => {
setSelectedEvent(event)
setEventDialogOpen(true)
}

// Handle event update/delete success
const handleEventUpdateSuccess = () => {
setEventDialogOpen(false)
setSelectedEvent(null)
refetchEvents()
}

// Handle shift template creation success
const handleShiftTemplateSuccess = () => {
Expand Down Expand Up @@ -479,7 +519,7 @@ export function CalendarGrid({
))}
</div>

{/* Overlay layer for shift templates */}
{/* Overlay layer for shift templates and events */}
<OverlayLayer
weekDates={weekDates}
shiftTemplates={shiftTemplates}
Expand All @@ -495,6 +535,8 @@ export function CalendarGrid({
roles={roles}
roleMap={roleMap}
rolesLoading={rolesLoading}
events={events}
onEventClick={handleEventClick}
/>

{/* Selection highlight overlay */}
Expand Down Expand Up @@ -531,11 +573,19 @@ export function CalendarGrid({
side={clickPosition.side}
align="start"
sideOffset={-0.5}
className="w-48 p-2"
className={showEventForm ? "w-80 p-0" : "w-48 p-2"}
>
<CalendarClickMenu
onCreateShift={handleCreateShift}
onCreateEvent={handleCreateEvent}
showEventForm={showEventForm}
restaurantId={selectedRestaurantId}
date={selection?.date}
startTime={selection?.startTime}
endTime={selection?.endTime}
employees={employees}
onEventCreated={handleEventCreated}
onEventFormCancel={handleEventFormCancel}
/>
</PopoverContent>
</Popover>
Expand All @@ -553,6 +603,16 @@ export function CalendarGrid({
initialStartTime={selection?.startTime}
initialEndTime={selection?.endTime}
/>

{/* Event Edit Dialog */}
<EventEditDialog
restaurantId={selectedRestaurantId}
event={selectedEvent}
employees={employees}
isOpen={eventDialogOpen}
onOpenChange={setEventDialogOpen}
onSuccess={handleEventUpdateSuccess}
/>
</div>
);
}
Loading