mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-08 18:23:13 -04:00
Doing more refactoring.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
@@ -9,6 +9,19 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import {
|
||||
formatLocalDateTimeInput,
|
||||
roundToStrictlyFutureQuarter,
|
||||
} from "../utils/date";
|
||||
import { getEditBookingRoomId, formatBookingError } from "../helpers/booking";
|
||||
import {
|
||||
validateRoomId,
|
||||
validateStart,
|
||||
validateEnd,
|
||||
validateInvitees,
|
||||
validateInviteeEmail,
|
||||
validateRoomAvailability,
|
||||
} from "../helpers/validation";
|
||||
import {
|
||||
getRoomBookings,
|
||||
createBooking,
|
||||
@@ -53,51 +66,7 @@ interface BookingFormProps {
|
||||
|
||||
// BookingForm: form for selecting room, date, time, title, and invitees
|
||||
|
||||
// Format Date as 'YYYY-MM-DDTHH:mm' for datetime-local input
|
||||
function formatLocalDateTimeInput(date: Date): string {
|
||||
function pad(n: number): string {
|
||||
return n < 10 ? "0" + n : n.toString();
|
||||
}
|
||||
const yyyy = date.getFullYear();
|
||||
const mm = pad(date.getMonth() + 1);
|
||||
const dd = pad(date.getDate());
|
||||
const hh = pad(date.getHours());
|
||||
const min = pad(date.getMinutes());
|
||||
return `${yyyy}-${mm}-${dd}T${hh}:${min}`;
|
||||
}
|
||||
|
||||
// Extract room_id from editBooking
|
||||
function getEditBookingRoomId(editBooking: any): string {
|
||||
// Try FullCalendar event object structure first
|
||||
let id =
|
||||
editBooking?._def?.extendedProps?.resource?.room_id ??
|
||||
editBooking?._def?.extendedProps?.resource?.id ??
|
||||
editBooking?.resource?.room_id ??
|
||||
editBooking?.room_id ??
|
||||
editBooking?.room?.id ??
|
||||
(typeof editBooking === "object" && "roomId" in editBooking
|
||||
? editBooking.roomId
|
||||
: undefined) ??
|
||||
(typeof editBooking === "object" &&
|
||||
"room" in editBooking &&
|
||||
editBooking.room &&
|
||||
"id" in editBooking.room
|
||||
? editBooking.room.id
|
||||
: undefined) ??
|
||||
"";
|
||||
if (!id && typeof editBooking === "object") {
|
||||
for (const k of Object.keys(editBooking)) {
|
||||
if (
|
||||
k.toLowerCase().includes("room") &&
|
||||
typeof editBooking[k] === "number"
|
||||
) {
|
||||
id = editBooking[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return id !== undefined && id !== null ? String(id) : "";
|
||||
}
|
||||
// ...existing code...
|
||||
|
||||
const BookingForm: React.FC<BookingFormProps> = ({
|
||||
open,
|
||||
@@ -413,88 +382,55 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [room_id, start, end, isEdit, editBooking?.id]);
|
||||
|
||||
// Helper: round a Date up to the next strictly future quarter hour
|
||||
function roundToStrictlyFutureQuarter(dt: Date): Date {
|
||||
const copy = new Date(dt.getTime());
|
||||
copy.setSeconds(0, 0);
|
||||
let add = 15 - (copy.getMinutes() % 15);
|
||||
if (add === 0) add = 15;
|
||||
copy.setMinutes(copy.getMinutes() + add);
|
||||
return copy;
|
||||
}
|
||||
// ...existing code...
|
||||
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
|
||||
function validate() {
|
||||
// Independent field validation
|
||||
function validateFields() {
|
||||
const newErrors: { [key: string]: string } = {};
|
||||
if (!room_id) newErrors.room_id = "Room is required.";
|
||||
if (!start) newErrors.start = "Start time is required.";
|
||||
if (!end) newErrors.end = "End time is required.";
|
||||
if (start && end && new Date(start) >= new Date(end))
|
||||
newErrors.end = "End time must be after start time.";
|
||||
if (invitees.length > roomCapacity) {
|
||||
newErrors.invitees = `The number of invitees exceeds the room's capacity (${roomCapacity}).`;
|
||||
}
|
||||
// Room ID
|
||||
const roomIdError = validateRoomId(room_id);
|
||||
if (roomIdError) newErrors.room_id = roomIdError;
|
||||
// Start
|
||||
const startError = validateStart(start);
|
||||
if (startError) newErrors.start = startError;
|
||||
// End
|
||||
const endError = validateEnd(end, start);
|
||||
if (endError) newErrors.end = endError;
|
||||
// Invitees
|
||||
const inviteesError = validateInvitees(invitees, roomCapacity);
|
||||
if (inviteesError) newErrors.invitees = inviteesError;
|
||||
// Individual invitee emails
|
||||
let hasInviteeError = false;
|
||||
invitees.forEach((email, idx) => {
|
||||
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
|
||||
newErrors[`invitee_${idx}`] = `Invalid email: ${email}`;
|
||||
const emailError = validateInviteeEmail(email);
|
||||
if (emailError) {
|
||||
newErrors[`invitee_${idx}`] = emailError;
|
||||
hasInviteeError = true;
|
||||
}
|
||||
});
|
||||
if (hasInviteeError) {
|
||||
newErrors.invitees = "One or more invitees have invalid email addresses.";
|
||||
}
|
||||
|
||||
// Overlap validation for start/end time (selected room)
|
||||
// Room availability
|
||||
if (room_id && start && end && roomBookings.length > 0) {
|
||||
// Filter out the current booking if editing
|
||||
const filteredBookings =
|
||||
isEdit && editBooking?.id
|
||||
? roomBookings.filter((b: any) => b.id !== editBooking.id)
|
||||
: roomBookings;
|
||||
// Only mark as conflict if there is actual overlap (not back-to-back)
|
||||
let startConflict = false;
|
||||
let endConflict = false;
|
||||
filteredBookings.forEach((b: any) => {
|
||||
// Overlap if start < b.end_time && end > b.start_time
|
||||
if (
|
||||
new Date(start) < new Date(b.end_time) &&
|
||||
new Date(end) > new Date(b.start_time)
|
||||
) {
|
||||
// Mark which field overlaps
|
||||
if (
|
||||
new Date(start) < new Date(b.end_time) &&
|
||||
new Date(start) >= new Date(b.start_time)
|
||||
) {
|
||||
startConflict = true;
|
||||
}
|
||||
if (
|
||||
new Date(end) > new Date(b.start_time) &&
|
||||
new Date(end) <= new Date(b.end_time)
|
||||
) {
|
||||
endConflict = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (startConflict || endConflict) {
|
||||
newErrors.room_id = "Room is unavailable for the selected time.";
|
||||
} else {
|
||||
// If no conflict, ensure room_id error is cleared
|
||||
if (newErrors.room_id) delete newErrors.room_id;
|
||||
}
|
||||
if (startConflict) {
|
||||
newErrors.start = "Start time overlaps with another booking.";
|
||||
}
|
||||
if (endConflict) {
|
||||
newErrors.end = "End time overlaps with another booking.";
|
||||
}
|
||||
const availabilityErrors = validateRoomAvailability(
|
||||
room_id,
|
||||
start,
|
||||
end,
|
||||
roomBookings,
|
||||
isEdit,
|
||||
editBooking
|
||||
);
|
||||
if (availabilityErrors.roomError)
|
||||
newErrors.room_id = availabilityErrors.roomError;
|
||||
if (availabilityErrors.startError)
|
||||
newErrors.start = availabilityErrors.startError;
|
||||
if (availabilityErrors.endError)
|
||||
newErrors.end = availabilityErrors.endError;
|
||||
}
|
||||
if (start && new Date(start) < new Date()) {
|
||||
newErrors.start = "Start time cannot be in the past.";
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
logger.warn("[BookingForm] Validation failed", newErrors);
|
||||
@@ -533,7 +469,7 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
|
||||
// Move this effect after roomBookings is defined
|
||||
useEffect(() => {
|
||||
validate();
|
||||
validateFields();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [room_id, invitees, start, end, roomBookings]);
|
||||
|
||||
@@ -542,7 +478,7 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
if (submitting) return;
|
||||
setSubmitting(true);
|
||||
setSubmitError(null);
|
||||
if (!validate()) {
|
||||
if (!validateFields()) {
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
@@ -663,18 +599,7 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to format booking errors for user-friendly messages
|
||||
function formatBookingError(detail: string): string {
|
||||
if (!detail) return "An unknown error occurred.";
|
||||
const lower = detail.toLowerCase();
|
||||
if (lower.includes("overlap")) {
|
||||
return "The selected room is already booked for the chosen time. Please pick a different time or room.";
|
||||
}
|
||||
if (lower.includes("capacity") || lower.includes("attendee")) {
|
||||
return "The number of invitees exceeds the room's capacity. Please remove some invitees or choose a larger room.";
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
// ...existing code...
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!isEdit) return;
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
import React from "react";
|
||||
import { List, Typography, ListItemButton } from "@mui/material";
|
||||
import type { BookingListProps } from "../schemas";
|
||||
import {
|
||||
sortBookingsByStartTime,
|
||||
formatBookingTime,
|
||||
getInviteeName,
|
||||
} from "../helpers/bookingList";
|
||||
|
||||
/**
|
||||
* BookingList component.
|
||||
@@ -43,35 +48,11 @@ const BookingList: React.FC<BookingListProps> = ({ bookings, onSelect }) => {
|
||||
);
|
||||
}
|
||||
// Sort bookings by start_time ascending (oldest first)
|
||||
const sortedBookings = [...bookings].sort(
|
||||
(a, b) =>
|
||||
new Date(a.start_time).getTime() - new Date(b.start_time).getTime()
|
||||
);
|
||||
const sortedBookings = sortBookingsByStartTime(bookings);
|
||||
return (
|
||||
<List sx={{ mt: 0.5 }}>
|
||||
{sortedBookings.map((booking) => {
|
||||
// Format time as h:mm AM/PM
|
||||
const formatTime = (dateStr: string) => {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleTimeString([], {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
};
|
||||
// Robust invitee name extraction
|
||||
const getInviteeName = (inv: any) => {
|
||||
if (typeof inv === "string") return inv;
|
||||
if (inv && typeof inv === "object") {
|
||||
if (typeof inv.name === "string") return inv.name;
|
||||
if (typeof inv.username === "string") return inv.username;
|
||||
if (typeof inv.displayName === "string") return inv.displayName;
|
||||
// Check for nested user object with name
|
||||
if (inv.user && typeof inv.user.name === "string")
|
||||
return inv.user.name;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
const inviteeNames = booking.invitees
|
||||
? booking.invitees.map(getInviteeName).filter(Boolean)
|
||||
: [];
|
||||
@@ -85,9 +66,9 @@ const BookingList: React.FC<BookingListProps> = ({ bookings, onSelect }) => {
|
||||
style={{ fontWeight: 600, color: "#23272f", fontSize: "1rem" }}
|
||||
>
|
||||
{booking.start_time && booking.end_time
|
||||
? `${formatTime(booking.start_time)} – ${formatTime(
|
||||
booking.end_time
|
||||
)}`
|
||||
? `${formatBookingTime(
|
||||
booking.start_time
|
||||
)} ${formatBookingTime(booking.end_time)}`
|
||||
: "Time not set"}
|
||||
</span>
|
||||
{inviteeNames.length > 0 && (
|
||||
|
||||
@@ -19,6 +19,7 @@ import { EventInput } from "@fullcalendar/core";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import "./CalendarView.css";
|
||||
import { getEventDisplayText, getRoomClass } from "../helpers/calendar";
|
||||
|
||||
/**
|
||||
* Props for CalendarView.
|
||||
@@ -53,23 +54,8 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
extendedProps?.resource?.roomId ||
|
||||
"Unknown";
|
||||
const invitees = extendedProps?.resource?.invitees || [];
|
||||
// Format start time as HH:mm
|
||||
const startTime = start
|
||||
? new Date(start).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: "-";
|
||||
// Display: '<start time> - <title>' or '<start time> - <room name>'
|
||||
const displayText =
|
||||
title && title.trim() !== ""
|
||||
? `${startTime} - ${title}`
|
||||
: `${startTime} - ${room}`;
|
||||
// Assign CSS class for room color
|
||||
const roomIdx = event.extendedProps?.resource?.room_id
|
||||
? parseInt(event.extendedProps.resource.room_id, 10) % 8
|
||||
: 0;
|
||||
const roomClass = `room-color-${roomIdx}`;
|
||||
const displayText = getEventDisplayText(title, start, room);
|
||||
const roomClass = getRoomClass(extendedProps?.resource?.room_id);
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import MeetingRoomIcon from "@mui/icons-material/MeetingRoom";
|
||||
import { FC } from "react";
|
||||
import type { RoomListProps } from "../schemas";
|
||||
import { getRoomListItemStyles, getRoomSecondaryText } from "../helpers/room";
|
||||
|
||||
/**
|
||||
* Renders a list of conference rooms with their details.
|
||||
@@ -99,21 +100,11 @@ const RoomList: FC<RoomListProps> = ({
|
||||
}}
|
||||
aria-label={`Select room ${room.name}`}
|
||||
data-testid={`room-item-${room.id}`}
|
||||
sx={
|
||||
room.id === selectedRoomId
|
||||
? {
|
||||
backgroundColor: "#ececec !important",
|
||||
fontWeight: 700,
|
||||
color: "#23272f",
|
||||
border: "1px solid #23272f",
|
||||
borderRadius: 4,
|
||||
}
|
||||
: { color: "#23272f" }
|
||||
}
|
||||
sx={getRoomListItemStyles(room.id, selectedRoomId)}
|
||||
>
|
||||
<ListItemText
|
||||
primary={room.name}
|
||||
secondary={`Location: ${room.location} | Capacity: ${room.capacity} | Equipment: ${room.equipment}`}
|
||||
secondary={getRoomSecondaryText(room)}
|
||||
/>
|
||||
</ListItemButton>
|
||||
) : (
|
||||
@@ -124,7 +115,7 @@ const RoomList: FC<RoomListProps> = ({
|
||||
>
|
||||
<ListItemText
|
||||
primary={room.name}
|
||||
secondary={`Location: ${room.location} | Capacity: ${room.capacity} | Equipment: ${room.equipment}`}
|
||||
secondary={getRoomSecondaryText(room)}
|
||||
/>
|
||||
</ListItem>
|
||||
)
|
||||
|
||||
45
frontend/src/helpers/booking.ts
Normal file
45
frontend/src/helpers/booking.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// booking.ts
|
||||
// Utility functions for booking-related logic
|
||||
|
||||
export function getEditBookingRoomId(editBooking: any): string {
|
||||
let id =
|
||||
editBooking?._def?.extendedProps?.resource?.room_id ??
|
||||
editBooking?._def?.extendedProps?.resource?.id ??
|
||||
editBooking?.resource?.room_id ??
|
||||
editBooking?.room_id ??
|
||||
editBooking?.room?.id ??
|
||||
(typeof editBooking === "object" && "roomId" in editBooking
|
||||
? editBooking.roomId
|
||||
: undefined) ??
|
||||
(typeof editBooking === "object" &&
|
||||
"room" in editBooking &&
|
||||
editBooking.room &&
|
||||
"id" in editBooking.room
|
||||
? editBooking.room.id
|
||||
: undefined) ??
|
||||
"";
|
||||
if (!id && typeof editBooking === "object") {
|
||||
for (const k of Object.keys(editBooking)) {
|
||||
if (
|
||||
k.toLowerCase().includes("room") &&
|
||||
typeof editBooking[k] === "number"
|
||||
) {
|
||||
id = editBooking[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return id !== undefined && id !== null ? String(id) : "";
|
||||
}
|
||||
|
||||
export function formatBookingError(detail: string): string {
|
||||
if (!detail) return "An unknown error occurred.";
|
||||
const lower = detail.toLowerCase();
|
||||
if (lower.includes("overlap")) {
|
||||
return "The selected room is already booked for the chosen time. Please pick a different time or room.";
|
||||
}
|
||||
if (lower.includes("capacity") || lower.includes("attendee")) {
|
||||
return "The number of invitees exceeds the room's capacity. Please remove some invitees or choose a larger room.";
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
29
frontend/src/helpers/bookingList.ts
Normal file
29
frontend/src/helpers/bookingList.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// bookingList.ts
|
||||
// Utility functions for BookingList component
|
||||
|
||||
export function sortBookingsByStartTime(bookings: any[]): any[] {
|
||||
return [...bookings].sort(
|
||||
(a, b) =>
|
||||
new Date(a.start_time).getTime() - new Date(b.start_time).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
export function formatBookingTime(dateStr: string): string {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleTimeString([], {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function getInviteeName(inv: any): string {
|
||||
if (typeof inv === "string") return inv;
|
||||
if (inv && typeof inv === "object") {
|
||||
if (typeof inv.name === "string") return inv.name;
|
||||
if (typeof inv.username === "string") return inv.username;
|
||||
if (typeof inv.displayName === "string") return inv.displayName;
|
||||
if (inv.user && typeof inv.user.name === "string") return inv.user.name;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
20
frontend/src/helpers/calendar.ts
Normal file
20
frontend/src/helpers/calendar.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// calendar.ts
|
||||
// Utility functions for CalendarView component
|
||||
|
||||
export function getEventDisplayText(
|
||||
title: string,
|
||||
start: Date,
|
||||
room: string
|
||||
): string {
|
||||
const startTime = start
|
||||
? start.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
||||
: "-";
|
||||
return title && title.trim() !== ""
|
||||
? `${startTime} - ${title}`
|
||||
: `${startTime} - ${room}`;
|
||||
}
|
||||
|
||||
export function getRoomClass(roomId: any): string {
|
||||
const idx = roomId ? parseInt(roomId, 10) % 8 : 0;
|
||||
return `room-color-${idx}`;
|
||||
}
|
||||
19
frontend/src/helpers/room.ts
Normal file
19
frontend/src/helpers/room.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// room.ts
|
||||
// Utility functions for RoomList component
|
||||
|
||||
export function getRoomSecondaryText(room: any): string {
|
||||
return `Location: ${room.location} | Capacity: ${room.capacity} | Equipment: ${room.equipment}`;
|
||||
}
|
||||
|
||||
export function getRoomListItemStyles(roomId: any, selectedRoomId: any): any {
|
||||
if (roomId === selectedRoomId) {
|
||||
return {
|
||||
backgroundColor: "#ececec !important",
|
||||
fontWeight: 700,
|
||||
color: "#23272f",
|
||||
border: "1px solid #23272f",
|
||||
borderRadius: 4,
|
||||
};
|
||||
}
|
||||
return { color: "#23272f" };
|
||||
}
|
||||
91
frontend/src/helpers/validation.ts
Normal file
91
frontend/src/helpers/validation.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// validation.ts
|
||||
// Independent field validation helpers for BookingForm
|
||||
|
||||
export function validateRoomId(room_id: string | undefined): string | null {
|
||||
if (!room_id) return "Room is required.";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateStart(start: string | undefined): string | null {
|
||||
if (!start) return "Start time is required.";
|
||||
if (start && new Date(start) < new Date())
|
||||
return "Start time cannot be in the past.";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateEnd(
|
||||
end: string | undefined,
|
||||
start?: string
|
||||
): string | null {
|
||||
if (!end) return "End time is required.";
|
||||
if (start && end && new Date(start) >= new Date(end))
|
||||
return "End time must be after start time.";
|
||||
if (end && new Date(end) < new Date())
|
||||
return "End time cannot be in the past.";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateInvitees(
|
||||
invitees: string[],
|
||||
roomCapacity: number
|
||||
): string | null {
|
||||
if (invitees.length > roomCapacity) {
|
||||
return `The number of invitees exceeds the room's capacity (${roomCapacity}).`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateInviteeEmail(email: string): string | null {
|
||||
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
|
||||
return `Invalid email: ${email}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateRoomAvailability(
|
||||
room_id: string,
|
||||
start: string,
|
||||
end: string,
|
||||
roomBookings: any[],
|
||||
isEdit: boolean,
|
||||
editBooking: any
|
||||
): { roomError?: string; startError?: string; endError?: string } {
|
||||
// Filter out the current booking if editing
|
||||
const filteredBookings =
|
||||
isEdit && editBooking?.id
|
||||
? roomBookings.filter((b: any) => b.id !== editBooking.id)
|
||||
: roomBookings;
|
||||
let startConflict = false;
|
||||
let endConflict = false;
|
||||
filteredBookings.forEach((b: any) => {
|
||||
if (
|
||||
new Date(start) < new Date(b.end_time) &&
|
||||
new Date(end) > new Date(b.start_time)
|
||||
) {
|
||||
if (
|
||||
new Date(start) < new Date(b.end_time) &&
|
||||
new Date(start) >= new Date(b.start_time)
|
||||
) {
|
||||
startConflict = true;
|
||||
}
|
||||
if (
|
||||
new Date(end) > new Date(b.start_time) &&
|
||||
new Date(end) <= new Date(b.end_time)
|
||||
) {
|
||||
endConflict = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
const errors: { roomError?: string; startError?: string; endError?: string } =
|
||||
{};
|
||||
if (startConflict || endConflict) {
|
||||
errors.roomError = "Room is unavailable for the selected time.";
|
||||
}
|
||||
if (startConflict) {
|
||||
errors.startError = "Start time overlaps with another booking.";
|
||||
}
|
||||
if (endConflict) {
|
||||
errors.endError = "End time overlaps with another booking.";
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
23
frontend/src/utils/date.ts
Normal file
23
frontend/src/utils/date.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// date.ts
|
||||
// Utility functions for date and time formatting and manipulation
|
||||
|
||||
export function formatLocalDateTimeInput(date: Date): string {
|
||||
function pad(n: number): string {
|
||||
return n < 10 ? "0" + n : n.toString();
|
||||
}
|
||||
const yyyy = date.getFullYear();
|
||||
const mm = pad(date.getMonth() + 1);
|
||||
const dd = pad(date.getDate());
|
||||
const hh = pad(date.getHours());
|
||||
const min = pad(date.getMinutes());
|
||||
return `${yyyy}-${mm}-${dd}T${hh}:${min}`;
|
||||
}
|
||||
|
||||
export function roundToStrictlyFutureQuarter(dt: Date): Date {
|
||||
const copy = new Date(dt.getTime());
|
||||
copy.setSeconds(0, 0);
|
||||
let add = 15 - (copy.getMinutes() % 15);
|
||||
if (add === 0) add = 15;
|
||||
copy.setMinutes(copy.getMinutes() + add);
|
||||
return copy;
|
||||
}
|
||||
Reference in New Issue
Block a user