diff --git a/src/app/api/user/[user]/calendar/route.ts b/src/app/api/user/[user]/calendar/route.ts
index f6b6098..bf01738 100644
--- a/src/app/api/user/[user]/calendar/route.ts
+++ b/src/app/api/user/[user]/calendar/route.ts
@@ -136,15 +136,15 @@ export const GET = auth(async function GET(req, { params }) {
start_time: 'asc',
},
select: {
- id: requestUserId === requestedUserId ? true : false,
- reason: requestUserId === requestedUserId ? true : false,
+ id: true,
+ reason: true,
start_time: true,
end_time: true,
- is_recurring: requestUserId === requestedUserId ? true : false,
- recurrence_end_date: requestUserId === requestedUserId ? true : false,
- rrule: requestUserId === requestedUserId ? true : false,
- created_at: requestUserId === requestedUserId ? true : false,
- updated_at: requestUserId === requestedUserId ? true : false,
+ is_recurring: true,
+ recurrence_end_date: true,
+ rrule: true,
+ created_at: true,
+ updated_at: true,
},
},
},
@@ -167,6 +167,7 @@ export const GET = auth(async function GET(req, { params }) {
calendar.push({ ...event.meeting, type: 'event' });
} else {
calendar.push({
+ id: event.meeting.id,
start_time: event.meeting.start_time,
end_time: event.meeting.end_time,
type: 'blocked_private',
@@ -182,6 +183,7 @@ export const GET = auth(async function GET(req, { params }) {
calendar.push({ ...event, type: 'event' });
} else {
calendar.push({
+ id: event.id,
start_time: event.start_time,
end_time: event.end_time,
type: 'blocked_private',
@@ -190,19 +192,27 @@ export const GET = auth(async function GET(req, { params }) {
}
for (const slot of requestedUser.blockedSlots) {
- calendar.push({
- start_time: slot.start_time,
- end_time: slot.end_time,
- id: slot.id,
- reason: slot.reason,
- is_recurring: slot.is_recurring,
- recurrence_end_date: slot.recurrence_end_date,
- rrule: slot.rrule,
- created_at: slot.created_at,
- updated_at: slot.updated_at,
- type:
- requestUserId === requestedUserId ? 'blocked_owned' : 'blocked_private',
- });
+ if (requestUserId === requestedUserId) {
+ calendar.push({
+ start_time: slot.start_time,
+ end_time: slot.end_time,
+ id: slot.id,
+ reason: slot.reason,
+ is_recurring: slot.is_recurring,
+ recurrence_end_date: slot.recurrence_end_date,
+ rrule: slot.rrule,
+ created_at: slot.created_at,
+ updated_at: slot.updated_at,
+ type: 'blocked_owned',
+ });
+ } else {
+ calendar.push({
+ start_time: slot.start_time,
+ end_time: slot.end_time,
+ id: slot.id,
+ type: 'blocked_private',
+ });
+ }
}
return returnZodTypeCheckedResponse(UserCalendarResponseSchema, {
diff --git a/src/app/api/user/[user]/calendar/validation.ts b/src/app/api/user/[user]/calendar/validation.ts
index a0d179f..996307f 100644
--- a/src/app/api/user/[user]/calendar/validation.ts
+++ b/src/app/api/user/[user]/calendar/validation.ts
@@ -13,6 +13,7 @@ export const BlockedSlotSchema = zod
start_time: eventStartTimeSchema,
end_time: eventEndTimeSchema,
type: zod.literal('blocked_private'),
+ id: zod.string(),
})
.openapi('BlockedSlotSchema', {
description: 'Blocked time slot in the user calendar',
diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx
index 6e10dab..bfd8651 100644
--- a/src/components/calendar.tsx
+++ b/src/components/calendar.tsx
@@ -1,10 +1,16 @@
'use client';
-import { Calendar, momentLocalizer } from 'react-big-calendar';
+import { Calendar as RBCalendar, momentLocalizer } from 'react-big-calendar';
+import withDragAndDrop from 'react-big-calendar/lib/addons/dragAndDrop';
import moment from 'moment';
import '@/components/react-big-calendar.css';
import 'react-big-calendar/lib/addons/dragAndDrop/styles.css';
import CustomToolbar from '@/components/custom-toolbar';
+import React from 'react';
+import { useGetApiUserUserCalendar } from '@/generated/api/user/user';
+import { useRouter } from 'next/navigation';
+import { usePatchApiEventEventID } from '@/generated/api/event/event';
+import { useSession } from 'next-auth/react';
moment.updateLocale('en', {
week: {
@@ -13,25 +19,131 @@ moment.updateLocale('en', {
},
});
+const DaDRBCalendar = withDragAndDrop<
+ {
+ id: string;
+ start: Date;
+ end: Date;
+ },
+ {
+ id: string;
+ title: string;
+ }
+>(RBCalendar);
+
const localizer = momentLocalizer(moment);
-const MyCalendar = (props) => (
-
- ('week');
+ const [currentDate, setCurrentDate] = React.useState(new Date());
+ const router = useRouter();
+
+ const { data, refetch } = useGetApiUserUserCalendar(userId, {
+ start: moment(currentDate)
+ .startOf(
+ currentView === 'agenda'
+ ? 'month'
+ : currentView === 'work_week'
+ ? 'week'
+ : currentView,
+ )
+ .toISOString(),
+ end: moment(currentDate)
+ .endOf(
+ currentView === 'agenda'
+ ? 'month'
+ : currentView === 'work_week'
+ ? 'week'
+ : currentView,
+ )
+ .toISOString(),
+ });
+
+ const { mutate: patchEvent } = usePatchApiEventEventID();
+
+ return (
+ {
+ setCurrentDate(date);
+ }}
+ events={
+ data?.data.calendar.map((event) => ({
+ id: event.id,
+ title: event.type === 'event' ? event.title : 'Blocker',
+ start: new Date(event.start_time),
+ end: new Date(event.end_time),
+ })) ?? []
+ }
+ onSelectEvent={(event) => {
+ router.push(`/events/${event.id}`);
+ }}
+ onSelectSlot={(slotInfo) => {
+ router.push(
+ `/events/new?start=${slotInfo.start.toISOString()}&end=${slotInfo.end.toISOString()}`,
+ );
+ }}
+ resourceIdAccessor={(event) => event.id}
+ resourceTitleAccessor={(event) => event.title}
+ startAccessor={(event) => event.start}
+ endAccessor={(event) => event.end}
+ selectable={sesstion.data?.user?.id === userId}
+ onEventDrop={(event) => {
+ const { start, end, event: droppedEvent } = event;
+ const startISO = new Date(start).toISOString();
+ const endISO = new Date(end).toISOString();
+ patchEvent(
+ {
+ eventID: droppedEvent.id,
+ data: {
+ start_time: startISO,
+ end_time: endISO,
+ },
+ },
+ {
+ onSuccess: () => {
+ refetch();
+ },
+ onError: (error) => {
+ console.error('Error updating event:', error);
+ },
+ },
+ );
+ }}
+ onEventResize={(event) => {
+ const { start, end, event: resizedEvent } = event;
+ const startISO = new Date(start).toISOString();
+ const endISO = new Date(end).toISOString();
+ patchEvent(
+ {
+ eventID: resizedEvent.id,
+ data: {
+ start_time: startISO,
+ end_time: endISO,
+ },
+ },
+ {
+ onSuccess: () => {
+ refetch();
+ },
+ onError: (error) => {
+ console.error('Error resizing event:', error);
+ },
+ },
+ );
+ }}
/>
-
-);
-
-export default MyCalendar;
+ );
+}
diff --git a/src/components/custom-toolbar.css b/src/components/custom-toolbar.css
index 55e9b77..c55d47a 100644
--- a/src/components/custom-toolbar.css
+++ b/src/components/custom-toolbar.css
@@ -1,57 +1,20 @@
-/* custom-toolbar.css */
-
/* Container der Toolbar */
.custom-toolbar {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
- background-color: #ffffff;
- border: 1px solid #e0e0e0;
- /*border-radius: 8px;*/
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
-/*.custom-toolbar .view-change .view-switcher {
- display: flex;
- gap: 8px;
- justify-content: center;
-}
-
-.custom-toolbar .view-change .view-switcher button {
- padding: 8px 16px;
- background-color: #c1830d;
- /*border: 1px solid #ccc;*/
-/* border-radius: 11px;
- font-size: 12px;
- cursor: pointer;
- transition: background-color 0.2s, border-color 0.2s;
- height: 30px;
- margin-top: 3.5px;
- color: #ffffff;
-}
-
-.custom-toolbar .view-change .view-switcher button:hover:not(:disabled) {
- background-color: #e0e0e0;
- border-color: #999;
-}
-
-.custom-toolbar .view-change .view-switcher button:disabled {
- background-color: #d0d0d0;
- border-color: #aaa;
- cursor: default;
-}*/
-
/* Anzeige des aktuellen Datums (Monat und Jahr) */
.custom-toolbar .current-date {
font-weight: bold;
font-size: 12px;
text-align: center;
color: #ffffff;
- /*margin: 4px 0;*/
background-color: #717171;
- width: 178px;
height: 37px;
border-radius: 11px;
}
@@ -65,7 +28,6 @@
.custom-toolbar .navigation-controls button {
padding: 8px 12px;
- /*background-color: #2196F3;*/
color: #ffffff;
border: none;
border-radius: 11px;
@@ -95,7 +57,6 @@
.custom-toolbar .dropdowns select {
padding: 8px 12px;
- /*border: 1px solid #ccc;*/
border-radius: 11px;
font-size: 10px;
background-color: #555555;
@@ -108,9 +69,9 @@
border-color: #999;
}
-.right-section {
+.right-section,
+.view-switcher {
background-color: #717171;
- width: 393px;
height: 48px;
border-radius: 11px;
justify-items: center;
@@ -124,18 +85,11 @@
margin-bottom: 3.5px;
}
-/*.custom-toolbar .navigation-controls .today button {
- background-color: #c6c6c6;
- height: 30px;
- width: 100px;
- color: #000000;
- margin-top: 3.5px;
-}*/
-
-.view-change {
+.view-change,
+.right-section {
background-color: #717171;
height: 48px;
- width: 323px;
+ padding: 0 8px;
border-radius: 11px;
justify-items: center;
}
@@ -144,7 +98,6 @@
color: #000000;
background-color: #c6c6c6;
height: 36px;
- width: 85px;
border-radius: 11px;
font-size: 12px;
align-self: center;
@@ -152,7 +105,6 @@
.datepicker {
text-align: center;
- width: 85px;
height: 30px;
}
diff --git a/src/components/custom-toolbar.tsx b/src/components/custom-toolbar.tsx
index bcbb9f9..36c8fff 100644
--- a/src/components/custom-toolbar.tsx
+++ b/src/components/custom-toolbar.tsx
@@ -1,19 +1,19 @@
import React, { useState, useEffect } from 'react';
-import { format } from 'date-fns';
import './custom-toolbar.css';
-import { Button } from '@/components/custom-ui/button';
+import { Button } from '@/components/ui/button';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
+import { NavigateAction } from 'react-big-calendar';
interface CustomToolbarProps {
//Aktuell angezeigtes Datum
date: Date;
//Aktuelle Ansicht
- view: 'month' | 'week' | 'day' | 'agenda';
+ view: 'month' | 'week' | 'day' | 'agenda' | 'work_week';
- onNavigate: (action: string, newDate?: Date) => void;
+ onNavigate: (action: NavigateAction, newDate?: Date) => void;
//Ansichtwechsel
- onView: (newView: 'month' | 'week' | 'day' | 'agenda') => void;
+ onView: (newView: 'month' | 'week' | 'day' | 'agenda' | 'work_week') => void;
}
const CustomToolbar: React.FC = ({
@@ -76,27 +76,11 @@ const CustomToolbar: React.FC = ({
setSelectedYear(getISOWeekYear(date));
}, [date]);
- //Dropdown-Liste der Wochen
- const totalWeeks = getISOWeeksInYear(selectedYear);
- const weekOptions = Array.from({ length: totalWeeks }, (_, i) => i + 1);
-
- //Jahresliste
- const yearOptions = Array.from(
- { length: 21 },
- (_, i) => selectedYear - 10 + i,
- );
-
//Start (Montag) und Ende (Sonntag) der aktuell angezeigten Woche berechnen
const weekStartDate = getDateOfISOWeek(selectedWeek, selectedYear);
const weekEndDate = new Date(weekStartDate);
weekEndDate.setDate(weekStartDate.getDate() + 6);
- //Monat und Jahr von Start- und Enddatum ermitteln
- const monthStart = format(weekStartDate, 'MMMM');
- const monthEnd = format(weekEndDate, 'MMMM');
- const yearAtStart = format(weekStartDate, 'yyyy');
- const yearAtEnd = format(weekEndDate, 'yyyy');
-
//Ansichtwechsel
const handleViewChange = (newView: 'month' | 'week' | 'day' | 'agenda') => {
onView(newView);
@@ -134,7 +118,7 @@ const CustomToolbar: React.FC = ({
}
//Datum im DatePicker aktualisieren
setSelectedDate(newDate);
- onNavigate('SET_DATE', newDate);
+ onNavigate('DATE', newDate);
};
//Pfeiltaste nach Hinten
@@ -160,7 +144,7 @@ const CustomToolbar: React.FC = ({
}
//Datum im DatePicker aktualisieren
setSelectedDate(newDate);
- onNavigate('SET_DATE', newDate);
+ onNavigate('DATE', newDate);
};
const [selectedDate, setSelectedDate] = useState(new Date());
@@ -174,14 +158,14 @@ const CustomToolbar: React.FC = ({
setSelectedWeek(newWeek);
setSelectedYear(newYear);
const newDate = getDateOfISOWeek(newWeek, newYear);
- onNavigate('SET_DATE', newDate);
+ onNavigate('DATE', newDate);
} else if (view === 'day') {
- onNavigate('SET_DATE', date);
+ onNavigate('DATE', date);
} else if (view === 'month') {
const newDate = new Date(date.getFullYear(), date.getMonth(), 1);
- onNavigate('SET_DATE', newDate);
+ onNavigate('DATE', newDate);
} else if (view === 'agenda') {
- onNavigate('SET_DATE', date);
+ onNavigate('DATE', date);
}
}
};
diff --git a/src/components/react-big-calendar.css b/src/components/react-big-calendar.css
index 1aed689..fe7270b 100644
--- a/src/components/react-big-calendar.css
+++ b/src/components/react-big-calendar.css
@@ -546,7 +546,8 @@ button.rbc-input::-moz-focus-inner {
padding-right: 15px;
text-transform: lowercase;
}
-.rbc-agenda-view table.rbc-agenda-table tbody > tr > td + td {
+.rbc-agenda-view table.rbc-agenda-table tbody > tr > td + td,
+.rbc-agenda-view table.rbc-agenda-table tbody > tr > td.rbc-agenda-time-cell {
border-left: 1px solid #ddd;
}
.rbc-rtl .rbc-agenda-view table.rbc-agenda-table tbody > tr > td + td {
@@ -767,7 +768,6 @@ button.rbc-input::-moz-focus-inner {
-ms-flex: 1;
flex: 1;
width: 100%;
- border: 1px solid #ddd;
min-height: 0;
}
.rbc-time-view .rbc-time-gutter {
@@ -870,10 +870,18 @@ button.rbc-input::-moz-focus-inner {
-ms-flex-align: start;
align-items: flex-start;
width: 100%;
- border-top: 2px solid #717171; /*#ddd*/
overflow-y: auto;
position: relative;
}
+
+.rbc-time-header-content {
+ border-bottom: 2px solid #717171; /*#ddd*/
+}
+
+.rbc-time-column :last-child {
+ border-bottom: 0;
+}
+
.rbc-time-content > .rbc-time-gutter {
-webkit-box-flex: 0;
-ms-flex: none;