diff --git a/src/deskstar-backend/Deskstar/Controllers/BookingController.cs b/src/deskstar-backend/Deskstar/Controllers/BookingController.cs index 2f9888c2..2f3c99e6 100644 --- a/src/deskstar-backend/Deskstar/Controllers/BookingController.cs +++ b/src/deskstar-backend/Deskstar/Controllers/BookingController.cs @@ -221,6 +221,7 @@ public IActionResult CreateBooking([FromBody] BookingRequest bookingRequest) [HttpDelete("{bookingId}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [Produces("application/json")] @@ -240,7 +241,62 @@ public IActionResult DeleteBooking(string bookingId) { "User not found" => NotFound(e.Message), "Booking not found" => NotFound(e.Message), - "You are not allowed to delete this booking" => BadRequest(e.Message), + "You are not allowed to delete this booking" => Forbid(e.Message), + _ => Problem(statusCode: 500) + }; + } + } + + /// + /// Updates a Booking for Token-User + /// + /// Updated Booking in JSON Format + /// + /// Sample request: + /// Put /bookings/{bookingId} with JWT Token + /// + /// + /// Returns the updated booking + /// User not found + /// Booking not found + /// User is not allowed to update this booking + /// Desk is not available at that time + /// Bad Request + [HttpPut("{bookingId}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [Produces("application/json")] + public IActionResult UpdateBooking(string bookingId, [FromBody] UpdateBookingRequest updateBookingRequest) + { + if (updateBookingRequest.StartTime.Equals(DateTime.MinValue) || updateBookingRequest.EndTime.Equals(DateTime.MinValue)) + { + return BadRequest("Required fields are missing"); + } + + var userId = RequestInteractions.ExtractIdFromRequest(Request); + + // ToDo: require Frontend to Use Universaltime + updateBookingRequest.StartTime = updateBookingRequest.StartTime.ToLocalTime(); + updateBookingRequest.EndTime = updateBookingRequest.EndTime.ToLocalTime(); + + try + { + var booking = _bookingUsecases.UpdateBooking(userId, new Guid(bookingId), updateBookingRequest); + return Ok(); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return e.Message switch + { + "User not found" => NotFound(e.Message), + "Booking not found" => NotFound(e.Message), + "You are not allowed to update this booking" => Forbid(e.Message), + "Time slot not available" => Conflict(e.Message), _ => Problem(statusCode: 500) }; } diff --git a/src/deskstar-backend/Deskstar/Models/UpdateBookingRequest.cs b/src/deskstar-backend/Deskstar/Models/UpdateBookingRequest.cs new file mode 100644 index 00000000..22c6c3e0 --- /dev/null +++ b/src/deskstar-backend/Deskstar/Models/UpdateBookingRequest.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Deskstar.Models; +public class UpdateBookingRequest +{ + [Required] + public DateTime StartTime { get; set; } + [Required] + public DateTime EndTime { get; set; } +} \ No newline at end of file diff --git a/src/deskstar-backend/Deskstar/Usecases/BookingUsecases.cs b/src/deskstar-backend/Deskstar/Usecases/BookingUsecases.cs index 75565cf3..64cb866e 100644 --- a/src/deskstar-backend/Deskstar/Usecases/BookingUsecases.cs +++ b/src/deskstar-backend/Deskstar/Usecases/BookingUsecases.cs @@ -11,6 +11,7 @@ public interface IBookingUsecases public Booking CreateBooking(Guid userId, BookingRequest bookingRequest); int CountValidBookings(Guid userId, string direction, DateTime start, DateTime end); public Booking DeleteBooking(Guid userId, Guid bookingId); + public Booking UpdateBooking(Guid userId, Guid bookingId, UpdateBookingRequest updateBookingRequest); } public class BookingUsecases : IBookingUsecases @@ -138,4 +139,39 @@ public Booking DeleteBooking(Guid userId, Guid bookingId) _context.SaveChanges(); return booking; } + + public Booking UpdateBooking(Guid userId, Guid bookingId, UpdateBookingRequest updateBookingRequest) + { + var user = _context.Users.FirstOrDefault(u => u.UserId == userId); + if (user == null) + { + throw new ArgumentException("User not found"); + } + + var booking = _context.Bookings.FirstOrDefault(b => b.BookingId == bookingId); + if (booking == null) + { + throw new ArgumentException("Booking not found"); + } + + if (booking.UserId != userId) + { + throw new ArgumentException("You are not allowed to update this booking"); + } + + var bookings = _context.Bookings.Where(b => b.DeskId == booking.DeskId && b.BookingId != bookingId); + var timeSlotAvailable = bookings.All(b => b.StartTime >= updateBookingRequest.EndTime || b.EndTime <= updateBookingRequest.StartTime); + if (!timeSlotAvailable) + { + throw new ArgumentException("Time slot not available"); + } + + booking.StartTime = updateBookingRequest.StartTime; + booking.EndTime = updateBookingRequest.EndTime; + booking.Timestamp = DateTime.Now; + + _context.Bookings.Update(booking); + _context.SaveChanges(); + return booking; + } } \ No newline at end of file diff --git a/src/deskstar-backend/Teststar.Tests/Tests/BookingUsecasesTest.cs b/src/deskstar-backend/Teststar.Tests/Tests/BookingUsecasesTest.cs index 3ec57bab..78645f41 100644 --- a/src/deskstar-backend/Teststar.Tests/Tests/BookingUsecasesTest.cs +++ b/src/deskstar-backend/Teststar.Tests/Tests/BookingUsecasesTest.cs @@ -434,6 +434,241 @@ public void CreateBooking_WhenAvailable_ShouldReturnABooking() db.Database.EnsureDeleted(); } + [Test] + public void UpdateBooking_WhenUserNotExists_ShouldThrowAnArgumentException() + { + //setup + using var db = new DataContext(); + + var deskId = Guid.NewGuid(); + SetupMockData(db, deskId: deskId); + + //arrange + var logger = new Mock>(); + var usecases = new BookingUsecases(logger.Object, db); + + var updateBookingRequest = new UpdateBookingRequest + { + StartTime = DateTime.Now, + EndTime = DateTime.Now + }; + //act + try + { + var result = usecases.UpdateBooking(Guid.NewGuid(), Guid.NewGuid(), updateBookingRequest); + + //assert + Assert.Fail(); + } + catch (ArgumentException e) + { + Assert.That(e.Message, Is.EqualTo("User not found")); + } + + //cleanup + db.Database.EnsureDeleted(); + } + + [Test] + public void UpdateBooking_WhenBookingNotExists_ShouldThrowAnArgumentException() + { + //setup + using var db = new DataContext(); + + var userId = Guid.NewGuid(); + SetupMockData(db, userId: userId); + + //arrange + var logger = new Mock>(); + var usecases = new BookingUsecases(logger.Object, db); + + var updateBookingRequest = new UpdateBookingRequest + { + StartTime = DateTime.Now, + EndTime = DateTime.Now + }; + //act + try + { + var result = usecases.UpdateBooking(userId, Guid.NewGuid(), updateBookingRequest); + + //assert + Assert.Fail(); + } + catch (ArgumentException e) + { + Assert.That(e.Message, Is.EqualTo("Booking not found")); + } + + //cleanup + db.Database.EnsureDeleted(); + } + + [Test] + public void UpdateBooking_WhenBookingNotFromUser_ShouldThrowAnArgumentException() + { + //setup + using var db = new DataContext(); + + var userId1 = Guid.NewGuid(); + var userId2 = Guid.NewGuid(); + var deskId = Guid.NewGuid(); + var bookingId = Guid.NewGuid(); + + SetupMockData(db, userId: userId1, deskId: deskId); + SetupMockData(db, userId: userId2); + + var firstBooking = new Booking + { + BookingId = bookingId, + DeskId = deskId, + UserId = userId1, + Timestamp = DateTime.Now, + StartTime = DateTime.Now, + EndTime = DateTime.Now + }; + db.Add(firstBooking); + db.SaveChanges(); + + //arrange + var logger = new Mock>(); + var usecases = new BookingUsecases(logger.Object, db); + + var updateBookingRequest = new UpdateBookingRequest + { + StartTime = DateTime.Now, + EndTime = DateTime.Now + }; + + //act + try + { + var result = usecases.UpdateBooking(userId2, bookingId, updateBookingRequest); + + //assert + Assert.Fail(); + } + catch (ArgumentException e) + { + Assert.That(e.Message, Is.EqualTo("You are not allowed to update this booking")); + } + + //cleanup + db.Database.EnsureDeleted(); + } + + [Test] + public void UpdateBooking_WhenTimeslotBooked_ShouldThrowAnArgumentException() + { + //setup + using var db = new DataContext(); + + var userId = Guid.NewGuid(); + var deskId = Guid.NewGuid(); + var bookingId = Guid.NewGuid(); + + SetupMockData(db, userId: userId, deskId: deskId); + var fbStart = DateTime.Now.Add(TimeSpan.FromHours(1)); + var fbEnd = DateTime.Now.Add(TimeSpan.FromHours(2)); + + var firstBooking = new Booking + { + BookingId = bookingId, + DeskId = deskId, + UserId = userId, + Timestamp = DateTime.Now, + StartTime = DateTime.Now, + EndTime = DateTime.Now + }; + var secondBooking = new Booking + { + BookingId = Guid.NewGuid(), + DeskId = deskId, + UserId = userId, + Timestamp = DateTime.Now, + StartTime = fbStart, + EndTime = fbEnd + }; + db.Add(firstBooking); + db.Add(secondBooking); + db.SaveChanges(); + + //arrange + var logger = new Mock>(); + var usecases = new BookingUsecases(logger.Object, db); + + var updateBookingRequest = new UpdateBookingRequest + { + StartTime = fbStart, + EndTime = fbEnd + }; + //act + try + { + var result = usecases.UpdateBooking(userId, bookingId, updateBookingRequest); + + //assert + Assert.Fail(); + } + catch (ArgumentException e) + { + Assert.That(e.Message, Is.EqualTo("Time slot not available")); + } + + //cleanup + db.Database.EnsureDeleted(); + } + + [Test] + public void UpdateBooking_WhenAvailable_ShouldReturnABooking() + { + //setup + using var db = new DataContext(); + + var userId = Guid.NewGuid(); + var deskId = Guid.NewGuid(); + var bookingId = Guid.NewGuid(); + + SetupMockData(db, userId: userId, deskId: deskId); + var fbStart = DateTime.Now.Add(TimeSpan.FromHours(1)); + var fbEnd = DateTime.Now.Add(TimeSpan.FromHours(2)); + + var firstBooking = new Booking + { + BookingId = bookingId, + DeskId = deskId, + UserId = userId, + Timestamp = DateTime.Now, + StartTime = fbStart, + EndTime = fbEnd + }; + db.Add(firstBooking); + db.SaveChanges(); + + //arrange + var logger = new Mock>(); + var usecases = new BookingUsecases(logger.Object, db); + + var updateBookingRequest = new UpdateBookingRequest + { + StartTime = DateTime.Now, + EndTime = DateTime.Now + }; + //act + var result = usecases.UpdateBooking(userId, bookingId, updateBookingRequest); + + //assert + Assert.That(result, Is.Not.Null); + Assert.That(result.BookingId, Is.EqualTo(bookingId)); + Assert.That(result.UserId, Is.EqualTo(userId)); + Assert.That(result.DeskId, Is.EqualTo(deskId)); + Assert.That(result.StartTime, Is.EqualTo(updateBookingRequest.StartTime)); + Assert.That(result.EndTime, Is.EqualTo(updateBookingRequest.EndTime)); + + //cleanup + db.Database.EnsureDeleted(); + } + [Test] public void DeleteBooking_WhenUserNotExists_ShouldThrowAnArgumentException() { diff --git a/src/deskstar-frontend/components/BookingsTable.tsx b/src/deskstar-frontend/components/BookingsTable.tsx index e56a42db..b7ee8e62 100644 --- a/src/deskstar-frontend/components/BookingsTable.tsx +++ b/src/deskstar-frontend/components/BookingsTable.tsx @@ -1,5 +1,6 @@ import { IBooking } from "../types/booking"; import { FaTrashAlt, FaEdit } from "react-icons/fa"; +import { UpdateBookingModal } from "./UpdateBookingModal"; const BookingsTable = ({ bookings, @@ -8,7 +9,7 @@ const BookingsTable = ({ onCheckIn, }: { bookings: IBooking[]; - onEdit?: (booking: IBooking) => void; + onEdit?: (booking: IBooking, startTime: Date, endTime: Date) => void; onDelete?: Function; onCheckIn?: Function; }) => { @@ -72,9 +73,10 @@ const BookingTableEntry = ({ {endTime} {onEdit && ( - onEdit(booking)}> + - + + )} {onDelete && ( diff --git a/src/deskstar-frontend/components/UpdateBookingModal.tsx b/src/deskstar-frontend/components/UpdateBookingModal.tsx new file mode 100644 index 00000000..a75d7fb9 --- /dev/null +++ b/src/deskstar-frontend/components/UpdateBookingModal.tsx @@ -0,0 +1,122 @@ +import { useState } from "react"; +import { start } from "repl"; +import { IBooking } from "../types/booking"; + +interface UpdateBookingModalProps { + id: string; + booking: IBooking; + onUpdate: Function; +} + +export function UpdateBookingModal({ + id, + booking, + onUpdate, +}: UpdateBookingModalProps) { + const [startDateTime, setStartDateTime] = useState( + new Date(booking.startTime) + ); + const [endDateTime, setEndDateTime] = useState(new Date(booking.endTime)); + + let today = new Date(); + today.setHours(8, 0, 0, 0); + + + return ( + <> + + + + + x + + Update Booking + + + + + + + Start: + + + setStartDateTime(new Date(event.target.value)) + } + /> + + + + + End: + + { + return setEndDateTime(new Date(event.target.value)); + }} + /> + + + + onUpdate(booking, startDateTime, endDateTime)} + > + Update + + + + + > + ); +} + +interface InputFormProps { + label: string; + type?: string; + value?: string; + disabled?: boolean; +} + +function InputForm({ label, type, value, disabled }: InputFormProps) { + return ( + + + {label} + + + + ); +} + +function formatDateForInputField(date: Date) { + const offset = date.getTimezoneOffset(); + + return new Date(date.getTime() - offset * 60 * 1000) + .toISOString() + .substring(0, "YYYY-MM-DDTHH:MM".length); +} \ No newline at end of file diff --git a/src/deskstar-frontend/lib/api/BookingService.ts b/src/deskstar-frontend/lib/api/BookingService.ts index 67d2177b..75ea0d22 100644 --- a/src/deskstar-frontend/lib/api/BookingService.ts +++ b/src/deskstar-frontend/lib/api/BookingService.ts @@ -148,3 +148,30 @@ export async function deleteBooking(session: Session, bookingId: string) { return "success"; } } + +/** + * Update start and end time for a given booking + * @param session the user session + * @param bookingId + * @param startTime new start time for given booking + * @param endTime new end time for given booking + * @returns + */ +export async function updateBooking( + session: Session, + bookingId: string, + startTime: string, + endTime: string +){ + return await fetch(BACKEND_URL + `/bookings/${bookingId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${session.accessToken}`, + }, + body: JSON.stringify({ + startTime, + endTime + }) + }) +} \ No newline at end of file diff --git a/src/deskstar-frontend/pages/bookings/index.tsx b/src/deskstar-frontend/pages/bookings/index.tsx index 71629512..e5bb2f8f 100644 --- a/src/deskstar-frontend/pages/bookings/index.tsx +++ b/src/deskstar-frontend/pages/bookings/index.tsx @@ -4,7 +4,11 @@ import BookingsTable from "../../components/BookingsTable"; import { IBooking } from "../../types/booking"; import { unstable_getServerSession } from "next-auth"; import { authOptions } from "../api/auth/[...nextauth]"; -import { getBookings, deleteBooking } from "../../lib/api/BookingService"; +import { + getBookings, + deleteBooking, + updateBooking, +} from "../../lib/api/BookingService"; import { toast } from "react-toastify"; import { useRouter } from "next/router"; import { useState } from "react"; @@ -24,10 +28,6 @@ export default function Bookings({ const router = useRouter(); const [currentPage, setCurrentPage] = useState(0); - const [showAlertSuccess, setShowAlertSuccess] = useState(false); - const [showAlertError, setShowAlertError] = useState(false); - const [alertMessage, setAlertMessage] = useState(""); - const { data: session } = useSession(); const refreshData = (newPageNumber: number) => { @@ -38,7 +38,6 @@ export default function Bookings({ const onDelete = async (booking: IBooking) => { if (session == null) return; - //TODO: implement console.log(`Pressed delete on ${booking.bookingId}`); try { @@ -59,10 +58,41 @@ export default function Bookings({ toast.error("Error calling Server:" + error); } }; - - const onEdit = (booking: IBooking) => { - //TODO: implement - toast.success(`Pressed edit on ${booking.bookingId}`); + + const onEdit = async ( + booking: IBooking, + newStartTime: Date, + newEndTime: Date + ) => { + if (session == null) return; + const offset = newStartTime.getTimezoneOffset(); + + const update = { + startTime: new Date( + newStartTime.getTime() - offset * 60 * 1000 + ).toISOString(), + endTime: new Date( + newEndTime.getTime() - offset * 60 * 1000 + ).toISOString(), + }; + + try { + const response = await updateBooking( + session, + booking.bookingId, + update.startTime, + update.endTime + ); + console.log(response); + if (!response.ok) + return toast.error(`Error: ${response.status} ${response.statusText}`); + + toast.success(`Booking successfully updated.`); + + refreshData(currentPage); + } catch (error) { + toast.error("Error during update: " + error); + } }; return (