pmtool / src /components /meeting-room /BookingForm.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
19.1 kB
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Textarea } from "@/components/ui/textarea";
import { ComboboxMulti } from './ComboboxMulti';
import { cn } from "@/lib/utils";
import { format } from "date-fns";
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Badge } from "@/components/ui/badge";
import { CalendarIcon, Clock, AlertCircle } from "lucide-react";
import { MeetingRoom, Employee, RoomBookingCreateRequest, RoomBooking } from '@/types';
import { userSessionService, roomBookingService } from '@/lib/api';
interface BookingFormProps {
room?: MeetingRoom;
employees: Employee[];
onSuccess?: () => void;
onCancel?: () => void;
defaultValues?: {
startTime?: Date;
endTime?: Date;
};
existingBooking?: RoomBooking;
isEditing?: boolean;
}
const timeOptions = Array.from({ length: 24 * 4 }, (_, i) => {
const hour = Math.floor(i / 4);
const minute = (i % 4) * 15;
return {
value: `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`,
label: `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`,
};
});
const bookingFormSchema = z.object({
title: z.string().min(3, {
message: "Title must be at least 3 characters.",
}),
date: z.date({
required_error: "Please select a date.",
}),
startTime: z.string({
required_error: "Please select a start time.",
}),
endTime: z.string({
required_error: "Please select an end time.",
}),
attendees: z.array(z.number()).min(1, {
message: "Please select at least one attendee.",
}),
description: z.string().optional(),
// Status field is not editable by the user, it will be set to 'pending' automatically
}).refine((data) => {
// Convert times to numbers for comparison
const start = data.startTime.replace(':', '');
const end = data.endTime.replace(':', '');
return start < end;
}, {
message: "End time must be after start time",
path: ["endTime"],
});
type FormValues = z.infer<typeof bookingFormSchema>;
const BookingForm: React.FC<BookingFormProps> = ({
room,
employees,
onSuccess,
onCancel,
defaultValues,
existingBooking,
isEditing
}) => {
const navigate = useNavigate();
const currentUser = userSessionService.getSession();
const [isSubmitting, setIsSubmitting] = useState(false);
const form = useForm<FormValues>({
resolver: zodResolver(bookingFormSchema),
defaultValues: {
title: existingBooking?.title || '',
date: existingBooking?.startTime ? new Date(existingBooking.startTime) : defaultValues?.startTime || new Date(),
startTime: existingBooking?.startTime
? format(new Date(existingBooking.startTime), 'HH:mm')
: defaultValues?.startTime
? format(defaultValues.startTime, 'HH:mm')
: format(new Date(), 'HH:mm'),
endTime: existingBooking?.endTime
? format(new Date(existingBooking.endTime), 'HH:mm')
: defaultValues?.endTime
? format(defaultValues.endTime, 'HH:mm')
: format(new Date(new Date().getTime() + 60 * 60 * 1000), 'HH:mm'),
attendees: existingBooking?.attendees || currentUser ? [...(existingBooking?.attendees || []), currentUser?.employeeId] : [],
description: existingBooking?.description || '',
},
});
// Filter out the current user from attendees options and organize by name
const attendeeOptions = React.useMemo(() => {
// Make sure employees is an array
const safeEmployees = Array.isArray(employees) ? employees : [];
const attendees = safeEmployees
.filter(emp => emp.id !== currentUser?.employeeId)
.map(emp => ({
value: emp.id,
label: `${emp.firstName} ${emp.lastName}`,
}));
// Sort alphabetically by label
return attendees.sort((a, b) => a.label.localeCompare(b.label));
}, [employees, currentUser]);
const onSubmit = async (data: FormValues) => {
if (!room || !currentUser) {
console.error('Missing room or user data:', { room, currentUser });
return;
}
setIsSubmitting(true);
console.log('Submitting booking form with data:', data);
try {
// Combine date and time
const startDate = new Date(data.date);
const [startHours, startMinutes] = data.startTime.split(':').map(Number);
startDate.setHours(startHours, startMinutes, 0, 0);
const endDate = new Date(data.date);
const [endHours, endMinutes] = data.endTime.split(':').map(Number);
endDate.setHours(endHours, endMinutes, 0, 0);
// Ensure attendees array is valid
const attendees = Array.isArray(data.attendees) ? data.attendees : [];
// Ensure current user is included
if (!attendees.includes(currentUser.employeeId)) {
attendees.push(currentUser.employeeId);
}
if (isEditing && existingBooking) {
// Update existing booking
const bookingData = {
id: existingBooking.id,
roomId: room.id,
roomName: room.name,
title: data.title,
startTime: startDate.toISOString(),
endTime: endDate.toISOString(),
attendees: attendees,
description: data.description || "",
// Maintain the current status unless admin is changing it
status: existingBooking.status,
employeeId: currentUser.employeeId,
employeeName: `${currentUser.firstName} ${currentUser.lastName}`
};
console.log('Updating booking data:', bookingData);
await roomBookingService.updateBooking(bookingData);
console.log('Booking updated successfully');
alert(`Room "${room.name}" booking updated successfully!`);
} else {
// Create new booking
const bookingData: RoomBookingCreateRequest = {
roomId: room.id,
roomName: room.name,
employeeId: currentUser.employeeId,
employeeName: `${currentUser.firstName} ${currentUser.lastName}`,
title: data.title,
startTime: startDate.toISOString(),
endTime: endDate.toISOString(),
attendees: attendees,
description: data.description || "",
status: 'pending',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
console.log('Creating new booking with data:', bookingData);
await roomBookingService.createBooking(bookingData);
console.log('Booking created successfully');
alert(`Room "${room.name}" booked successfully!\n\nStatus: Pending Approval\n\nYour booking will be reviewed by an administrator.`);
}
// Use a timeout to ensure the alert has time to be displayed before navigation
setTimeout(() => {
if (onSuccess) {
onSuccess();
} else {
// Use relative navigation with hash to ensure page refresh
const baseUrl = window.location.origin;
const bookingsPath = "/meeting-room/bookings";
// Create a timestamp to force a page refresh
const timestamp = new Date().getTime();
const fullUrl = `${baseUrl}${bookingsPath}#${timestamp}`;
// Use full URL to navigate
window.location.href = fullUrl;
}
}, 500); // Increased timeout to give more time for alert to be seen
} catch (error) {
console.error('Failed to process booking:', error);
alert(`There was an error ${isEditing ? 'updating' : 'creating'} the booking. Please try again.`);
} finally {
setIsSubmitting(false);
}
};
return (
<Card className="w-full max-w-3xl mx-auto">
<CardHeader>
<CardTitle>{isEditing ? 'Edit Booking' : 'Book a Meeting Room'}</CardTitle>
<CardDescription>
{room ? (
<div className="flex flex-col gap-1 mt-1">
<div>Room: <span className="font-medium">{room.name}</span> (Capacity: {room.capacity})</div>
{!isEditing && (
<div className="flex items-center gap-2">
<span>Booking Status:</span>
<Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200">
<AlertCircle className="mr-1 h-3 w-3" /> Pending Approval
</Badge>
</div>
)}
{isEditing && existingBooking && (
<div className="flex items-center gap-2">
<span>Current Status:</span>
<Badge
variant={
existingBooking.status === 'approved' ? 'default' :
existingBooking.status === 'rejected' ? 'destructive' :
existingBooking.status === 'cancelled' ? 'outline' :
'secondary'
}
className="capitalize"
>
{existingBooking.status || 'pending'}
</Badge>
</div>
)}
</div>
) : (
'Complete the form to book a meeting room'
)}
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Meeting Title</FormLabel>
<FormControl>
<Input placeholder="Team standup" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Date</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full pl-3 text-left font-normal",
!field.value && "text-muted-foreground"
)}
>
{field.value ? (
format(field.value, "PPP")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="startTime"
render={({ field }) => (
<FormItem>
<FormLabel>Start Time</FormLabel>
<FormControl>
<div className="flex items-center">
<Clock className="mr-2 h-4 w-4 text-muted-foreground" />
<select
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
value={field.value}
onChange={field.onChange}
>
{timeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="endTime"
render={({ field }) => (
<FormItem>
<FormLabel>End Time</FormLabel>
<FormControl>
<div className="flex items-center">
<Clock className="mr-2 h-4 w-4 text-muted-foreground" />
<select
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
value={field.value}
onChange={field.onChange}
>
{timeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<FormField
control={form.control}
name="attendees"
render={({ field }) => {
// Make sure we have a valid array for the field value
const fieldValue = Array.isArray(field.value) ? field.value : [];
// Check if we have any attendee options
const hasOptions = Array.isArray(attendeeOptions) && attendeeOptions.length > 0;
// Get current user name for display
const currentUserName = currentUser ?
`${currentUser.firstName} ${currentUser.lastName}` :
'You';
return (
<FormItem>
<FormLabel>Meeting Attendees</FormLabel>
<FormControl>
<ComboboxMulti
options={hasOptions ? attendeeOptions : []}
selectedValues={fieldValue}
onValuesChange={(values) => {
try {
// Ensure values is an array
const safeValues = Array.isArray(values) ? values : [];
// Ensure current user is always included
if (currentUser && !safeValues.includes(currentUser.employeeId)) {
const updatedValues = [currentUser.employeeId, ...safeValues];
field.onChange(updatedValues);
} else {
field.onChange(safeValues);
}
} catch (error) {
console.error("Error updating attendees:", error);
// Fallback to empty array if there's an error
field.onChange(currentUser ? [currentUser.employeeId] : []);
}
}}
placeholder={hasOptions ? "Select meeting attendees..." : "No other attendees available"}
emptyText={hasOptions ? "No matching employees found" : "No other employees available"}
label="Attendees"
/>
</FormControl>
<FormDescription>
{currentUserName} will be automatically added as an attendee. {hasOptions ? 'Search for additional attendees by name.' : ''}
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description (Optional)</FormLabel>
<FormControl>
<Textarea
placeholder="Meeting agenda, preparation details, etc."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<CardFooter className="flex justify-between px-0 pb-0">
<Button
type="button"
variant="outline"
onClick={onCancel}
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting}
>
{isSubmitting
? (isEditing ? "Updating..." : "Booking...")
: (isEditing ? "Update Booking" : "Book Room")
}
</Button>
</CardFooter>
</form>
</Form>
</CardContent>
</Card>
);
};
export default BookingForm;