Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
53 changes: 53 additions & 0 deletions app/api/shifts/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,56 @@ export async function DELETE(
);
}
}

// PUT/UPDATE shift (requires write permission)
export async function PUT(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const user = await getSessionUser(request.headers);
const body = await request.json();

// Fetch shift to get calendar ID
const [existingShift] = await db.select().from(shifts).where(eq(shifts.id, id));

if (!existingShift) {
return NextResponse.json({ error: "Shift not found" }, { status: 404 });
}

// Check write permission (works for both authenticated users and guests)
const hasAccess = await canEditCalendar(user?.id, existingShift.calendarId);
if (!hasAccess) {
return NextResponse.json(
{ error: "Insufficient permissions. Write access required." },
{ status: 403 }
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Update the shift
const [updatedShift] = await db
.update(shifts)
.set({
date: body.date ? new Date(body.date) : existingShift.date,
startTime: body.startTime ?? existingShift.startTime,
endTime: body.endTime ?? existingShift.endTime,
title: body.title ?? existingShift.title,
color: body.color ?? existingShift.color,
notes: body.notes ?? existingShift.notes,
isAllDay: body.isAllDay ?? existingShift.isAllDay,
presetId: body.presetId ?? existingShift.presetId,
updatedAt: new Date(),
})
.where(eq(shifts.id, id))
.returning();

return NextResponse.json(updatedShift);
} catch (error) {
console.error("Failed to update shift:", error);
return NextResponse.json(
{ error: "Failed to update shift" },
{ status: 500 }
);
}
}
38 changes: 36 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { CalendarCompareView } from "@/components/calendar-compare-view";
import { AppFooter } from "@/components/app-footer";
import { AppHeader } from "@/components/app-header";
import { DialogManager } from "@/components/dialog-manager";
import { ShiftFormData } from "@/components/shift-sheet";
import { getCalendarDays } from "@/lib/calendar-utils";
import { formatDateToLocal, parseLocalDate } from "@/lib/date-utils";
import { findNotesForDate } from "@/lib/event-utils";
Expand Down Expand Up @@ -67,6 +68,7 @@ function HomeContent() {
hasLoadedOnce: shiftsLoadedOnce,
createShift: createShiftHook,
deleteShift: deleteShiftHook,
updateShift: updateShiftHook,
refetchShifts,
} = useShifts(selectedCalendar);

Expand All @@ -85,6 +87,7 @@ function HomeContent() {
const [compareNoteCalendarId, setCompareNoteCalendarId] = useState<
string | undefined
>();
const [editingShift, setEditingShift] = useState<ShiftWithCalendar | undefined>();

// Compare mode state (needs to be before useNotes hook)
const [isCompareMode, setIsCompareMode] = useState(false);
Expand Down Expand Up @@ -358,6 +361,34 @@ function HomeContent() {
refetchShifts();
};

// Handler for editing a shift from the day shifts dialog
const handleEditShiftFromDayDialog = (shift: ShiftWithCalendar) => {
setEditingShift(shift);
setSelectedDate(shift.date as Date);
dialogStates.setShowShiftDialog(true);
};

// Clear editing state when dialog closes
const handleShiftDialogChange = (open: boolean) => {
dialogStates.setShowShiftDialog(open);
if (!open) {
setEditingShift(undefined);
}
};

// Handle shift submit (create or update)
const handleShiftSubmit = async (formData: ShiftFormData) => {
if (editingShift) {
// Update existing shift
await updateShiftHook(editingShift.id, formData);
setEditingShift(undefined);
refetchShifts();
} else {
// Create new shift
await shiftActions.handleShiftSubmit(formData);
}
};

// Compare mode handlers
const handleCompareClick = () => {
setShowCompareSelector(true);
Expand Down Expand Up @@ -897,6 +928,7 @@ function HomeContent() {
onShowAllShifts={handleShowAllShifts}
onShowSyncedShifts={handleShowSyncedShifts}
onDeleteShift={shiftActions.handleDeleteShift}
onEditShift={handleEditShiftFromDayDialog}
/>
</div>

Expand Down Expand Up @@ -924,11 +956,12 @@ function HomeContent() {
onCalendarDialogChange={dialogStates.setShowCalendarDialog}
onCreateCalendar={createCalendarHook}
showShiftDialog={dialogStates.showShiftDialog}
onShiftDialogChange={dialogStates.setShowShiftDialog}
onShiftSubmit={shiftActions.handleShiftSubmit}
onShiftDialogChange={handleShiftDialogChange}
onShiftSubmit={handleShiftSubmit}
selectedDate={selectedDate}
selectedCalendar={selectedCalendar || null}
calendars={calendars}
editingShift={editingShift}
showCalendarSettingsDialog={dialogStates.showCalendarSettingsDialog}
onCalendarSettingsDialogChange={
dialogStates.setShowCalendarSettingsDialog
Expand All @@ -951,6 +984,7 @@ function HomeContent() {
selectedDayShifts={dialogStates.selectedDayShifts}
locale={locale}
onDeleteShiftFromDayDialog={handleDeleteShiftFromDayDialog}
onEditShiftFromDayDialog={handleEditShiftFromDayDialog}
showSyncedShiftsDialog={dialogStates.showSyncedShiftsDialog}
onSyncedShiftsDialogChange={dialogStates.setShowSyncedShiftsDialog}
selectedSyncedShifts={dialogStates.selectedSyncedShifts}
Expand Down
3 changes: 3 additions & 0 deletions components/calendar-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface CalendarContentProps {
onShowAllShifts: (date: Date, shifts: ShiftWithCalendar[]) => void;
onShowSyncedShifts: (date: Date, shifts: ShiftWithCalendar[]) => void;
onDeleteShift?: (id: string) => void;
onEditShift?: (shift: ShiftWithCalendar) => void;
}

export function CalendarContent(props: CalendarContentProps) {
Expand Down Expand Up @@ -87,6 +88,7 @@ export function CalendarContent(props: CalendarContentProps) {
onLongPress={props.onLongPress}
onShowAllShifts={props.onShowAllShifts}
onShowSyncedShifts={props.onShowSyncedShifts}
onEditShift={props.onEditShift}
/>

<motion.div
Expand Down Expand Up @@ -128,6 +130,7 @@ export function CalendarContent(props: CalendarContentProps) {
shifts={props.shifts}
currentDate={props.currentDate}
onDeleteShift={props.onDeleteShift}
onEditShift={props.onEditShift}
calendarId={props.selectedCalendar || undefined}
/>
</div>
Expand Down
104 changes: 50 additions & 54 deletions components/calendar-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface CalendarGridProps {
onLongPress?: (date: Date) => void;
onShowAllShifts?: (date: Date, shifts: ShiftWithCalendar[]) => void;
onShowSyncedShifts?: (date: Date, shifts: ShiftWithCalendar[]) => void;
onEditShift?: (shift: ShiftWithCalendar) => void;
}

export function CalendarGrid({
Expand All @@ -57,6 +58,7 @@ export function CalendarGrid({
onLongPress,
onShowAllShifts,
onShowSyncedShifts,
onEditShift,
}: CalendarGridProps) {
const t = useTranslations();
const pressTimerRef = useRef<Record<string, NodeJS.Timeout>>({});
Expand Down Expand Up @@ -174,9 +176,9 @@ export function CalendarGrid({
const eventBorderStyle =
dayEvents.length === 1 && !isTodayDate
? {
borderColor: dayEvents[0].color || "#3b82f6",
borderWidth: "2px",
}
borderColor: dayEvents[0].color || "#3b82f6",
borderWidth: "2px",
}
: {};

return (
Expand All @@ -200,9 +202,9 @@ export function CalendarGrid({
WebkitTouchCallout: "none",
...(isHighlighted &&
!isTodayDate && {
backgroundColor: `${highlightColor}15`,
borderColor: `${highlightColor}40`,
}),
backgroundColor: `${highlightColor}15`,
borderColor: `${highlightColor}40`,
}),
// Event border styling (overrides highlight if both present)
...eventBorderStyle,
// Multi-event gradient border using background trick to support border-radius
Expand All @@ -218,17 +220,15 @@ export function CalendarGrid({
className={`
min-h-25 sm:min-h-28 px-1 py-1.5 sm:p-2.5 rounded-md sm:rounded-lg text-sm transition-all relative flex flex-col border sm:border-2
${isCurrentMonth ? "text-foreground" : "text-muted-foreground/50"}
${
isTodayDate
? "border-primary shadow-lg shadow-primary/20 bg-primary/5 ring-2 ring-primary/20"
: dayEvent
${isTodayDate
? "border-primary shadow-lg shadow-primary/20 bg-primary/5 ring-2 ring-primary/20"
: dayEvent
? "" // Event border is handled by inline style
: "border-border/30 sm:border-border/50"
}
${
isCurrentMonth
? "hover:bg-accent cursor-pointer active:bg-accent/80 hover:border-border"
: selectedPresetId
${isCurrentMonth
? "hover:bg-accent cursor-pointer active:bg-accent/80 hover:border-border"
: selectedPresetId
? "cursor-not-allowed"
: "cursor-pointer"
}
Expand All @@ -237,20 +237,18 @@ export function CalendarGrid({
`}
>
<div
className={`text-sm sm:text-sm font-semibold mb-1 flex items-center justify-between gap-1 ${
isTodayDate ? "text-primary" : ""
}`}
className={`text-sm sm:text-sm font-semibold mb-1 flex items-center justify-between gap-1 ${isTodayDate ? "text-primary" : ""
}`}
>
<span className="shrink-0">{day.getDate()}</span>
<div className="flex items-center gap-1 min-w-0">
{/* Multi-indicator badge when multiple notes/events exist */}
{totalNotesCount > 1 && (
<span
className={`inline-flex items-center justify-center text-[9px] sm:text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-primary/20 text-primary border border-primary/30 ${
!selectedPresetId && onNoteIconClick
? "cursor-pointer hover:bg-primary/30 transition-colors"
: ""
}`}
className={`inline-flex items-center justify-center text-[9px] sm:text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-primary/20 text-primary border border-primary/30 ${!selectedPresetId && onNoteIconClick
? "cursor-pointer hover:bg-primary/30 transition-colors"
: ""
}`}
title={t("note.multipleEntries", {
count: totalNotesCount,
})}
Expand All @@ -267,11 +265,10 @@ export function CalendarGrid({
{/* Display first event title - clickable if no preset selected */}
{dayEvent && totalNotesCount === 1 && (
<span
className={`text-[10px] sm:text-xs font-medium truncate opacity-75 min-w-0 ${
!selectedPresetId && onNoteIconClick
? "cursor-pointer hover:opacity-100 transition-opacity"
: ""
}`}
className={`text-[10px] sm:text-xs font-medium truncate opacity-75 min-w-0 ${!selectedPresetId && onNoteIconClick
? "cursor-pointer hover:opacity-100 transition-opacity"
: ""
}`}
style={{ color: dayEvent.color || "#3b82f6" }}
title={dayEvent.note}
onClick={(e) => {
Expand All @@ -287,11 +284,10 @@ export function CalendarGrid({
{/* Display first note title if no event and only one entry - clickable if no preset selected */}
{!dayEvent && dayNote && totalNotesCount === 1 && (
<span
className={`text-[10px] sm:text-xs font-medium text-orange-500 truncate opacity-75 min-w-0 ${
!selectedPresetId && onNoteIconClick
? "cursor-pointer hover:opacity-100 transition-opacity"
: ""
}`}
className={`text-[10px] sm:text-xs font-medium text-orange-500 truncate opacity-75 min-w-0 ${!selectedPresetId && onNoteIconClick
? "cursor-pointer hover:opacity-100 transition-opacity"
: ""
}`}
title={dayNote.note}
onClick={(e) => {
if (!selectedPresetId && onNoteIconClick) {
Expand Down Expand Up @@ -384,10 +380,10 @@ export function CalendarGrid({
const hiddenExternalCount =
maxExternalShiftsToShow !== undefined
? Math.max(
0,
sortedExternalNormalShifts.length -
maxExternalShiftsToShow
)
0,
sortedExternalNormalShifts.length -
maxExternalShiftsToShow
)
: 0;
const totalHiddenCount =
hiddenRegularCount + hiddenExternalCount;
Expand Down Expand Up @@ -433,6 +429,7 @@ export function CalendarGrid({
shift={shift}
showShiftNotes={showShiftNotes}
showFullTitles={showFullTitles}
onEditShift={!selectedPresetId ? onEditShift : undefined}
/>
);
})}
Expand All @@ -446,11 +443,10 @@ export function CalendarGrid({
// Show all shifts dialog with all day shifts
onShowAllShifts?.(day, displayableShifts);
}}
className={`text-[10px] sm:text-xs text-primary font-semibold text-center pt-0.5 transition-colors ${
selectedPresetId
? "cursor-not-allowed opacity-50"
: "hover:text-primary/80 hover:underline cursor-pointer"
}`}
className={`text-[10px] sm:text-xs text-primary font-semibold text-center pt-0.5 transition-colors ${selectedPresetId
? "cursor-not-allowed opacity-50"
: "hover:text-primary/80 hover:underline cursor-pointer"
}`}
>
+{totalHiddenCount}{" "}
{totalHiddenCount === 1
Expand All @@ -472,22 +468,24 @@ export function CalendarGrid({
shift={shift}
showShiftNotes={showShiftNotes}
showFullTitles={showFullTitles}
onEditShift={!selectedPresetId ? onEditShift : undefined}
/>
))}

{/* Display external shifts with normal display mode */}
{(maxExternalShiftsToShow === undefined
? sortedExternalNormalShifts
: sortedExternalNormalShifts.slice(
0,
maxExternalShiftsToShow
)
0,
maxExternalShiftsToShow
)
).map((shift) => (
<CalendarShiftCard
key={shift.id}
shift={shift}
showShiftNotes={showShiftNotes}
showFullTitles={showFullTitles}
onEditShift={!selectedPresetId ? onEditShift : undefined}
/>
))}

Expand All @@ -500,11 +498,10 @@ export function CalendarGrid({
// Show all shifts dialog with all day shifts
onShowAllShifts?.(day, displayableShifts);
}}
className={`text-[10px] sm:text-xs text-primary font-semibold text-center pt-0.5 transition-colors ${
selectedPresetId
? "cursor-not-allowed opacity-50"
: "hover:text-primary/80 hover:underline cursor-pointer"
}`}
className={`text-[10px] sm:text-xs text-primary font-semibold text-center pt-0.5 transition-colors ${selectedPresetId
? "cursor-not-allowed opacity-50"
: "hover:text-primary/80 hover:underline cursor-pointer"
}`}
>
+{totalHiddenCount}{" "}
{totalHiddenCount === 1
Expand All @@ -529,11 +526,10 @@ export function CalendarGrid({
e.stopPropagation();
onShowSyncedShifts?.(day, syncShifts);
}}
className={`text-[10px] sm:text-xs px-1 py-0.5 sm:px-1.5 sm:py-1 rounded bg-muted/50 border border-border/50 text-muted-foreground transition-colors text-center ${
selectedPresetId
? "cursor-not-allowed opacity-50"
: "hover:bg-muted hover:text-foreground cursor-pointer"
}`}
className={`text-[10px] sm:text-xs px-1 py-0.5 sm:px-1.5 sm:py-1 rounded bg-muted/50 border border-border/50 text-muted-foreground transition-colors text-center ${selectedPresetId
? "cursor-not-allowed opacity-50"
: "hover:bg-muted hover:text-foreground cursor-pointer"
}`}
style={{
borderLeftColor: sync.color,
borderLeftWidth: "2px",
Expand Down
Loading