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;