import React, { useState, useEffect } from "react"; import { format, isSameDay, parseISO } from "date-fns"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { TimeLog, timeLogApi, TimeLogCreateRequest } from "@/services/timeLogApi"; import { Task, tasksApi } from "@/services/tasksApi"; import { useAuth } from "@/lib/auth-context"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { PopoverContent, PopoverTrigger, Popover } from "@/components/ui/popover"; import { Calendar } from "@/components/ui/calendar"; import { cn } from "@/lib/utils"; import { CalendarIcon, Check, Clock, Loader2, X, AlertCircle } from "lucide-react"; import { CustomSelect } from "@/components/ui/custom-select"; import { showToast } from "@/components/toast-utils"; import { CustomDateInput } from "@/components/ui/custom-date-input"; import { Dropdown } from "@/components/ui/dropdown"; import SlateRichTextEditor from "@/components/custom/SlateRichTextEditor"; interface AddTimeLogDialogProps { isOpen: boolean; onClose: () => void; taskId?: number; // Optional taskId - when provided, the task dropdown will be pre-selected and disabled onTimeLogAdded?: () => void; // Optional callback after successful creation editMode?: boolean; // Whether we're editing an existing time log timeLogToEdit?: TimeLog | null; // The time log to edit when in edit mode } export default function AddTimeLogDialog({ isOpen, onClose, taskId, onTimeLogAdded, editMode = false, timeLogToEdit = null }: AddTimeLogDialogProps) { const { userData } = useAuth(); const queryClient = useQueryClient(); // Form state const [workDate, setWorkDate] = useState(new Date()); const [startTime, setStartTime] = useState("9:30 AM"); const [endTime, setEndTime] = useState("10:00 AM"); const [timeLogHours, setTimeLogHours] = useState(""); const [remainingHours, setRemainingHours] = useState(""); const [timeLogDescription, setTimeLogDescription] = useState(""); const [selectedTask, setSelectedTask] = useState(taskId || null); const [timeLogId, setTimeLogId] = useState(null); // Validation state const [descriptionError, setDescriptionError] = useState(false); // Submission prevention state const [isSubmitting, setIsSubmitting] = useState(false); const [lastSubmissionTime, setLastSubmissionTime] = useState(0); // Function to calculate hours between two time strings with AM/PM format const calculateHoursSpent = (startTime: string, endTime: string): string => { if (!startTime || !endTime) return ""; // Parse start time const startParts = startTime.split(' '); const [startHourMin, startAmPm] = [startParts[0], startParts[1]]; let [startHour, startMinute] = startHourMin.split(':').map(Number); // Convert to 24-hour format for calculation if (startAmPm === 'PM' && startHour < 12) startHour += 12; if (startAmPm === 'AM' && startHour === 12) startHour = 0; // Parse end time const endParts = endTime.split(' '); const [endHourMin, endAmPm] = [endParts[0], endParts[1]]; let [endHour, endMinute] = endHourMin.split(':').map(Number); // Convert to 24-hour format for calculation if (endAmPm === 'PM' && endHour < 12) endHour += 12; if (endAmPm === 'AM' && endHour === 12) endHour = 0; // Convert to minutes then calculate the difference const startMinutes = startHour * 60 + startMinute; const endMinutes = endHour * 60 + endMinute; // Handle case where end time is on the next day const diffMinutes = endMinutes >= startMinutes ? endMinutes - startMinutes : (24 * 60 - startMinutes) + endMinutes; // Convert minutes to hours with two decimal places return (diffMinutes / 60).toFixed(2); }; // Add function to convert AM/PM time format to 24-hour format const convertTo24HourFormat = (timeStr: string): string => { if (!timeStr) return ""; const timeParts = timeStr.split(' '); const [hourMin, ampm] = [timeParts[0], timeParts[1]]; let [hour, minute] = hourMin.split(':').map(Number); // Convert to 24-hour format if (ampm === 'PM' && hour < 12) hour += 12; if (ampm === 'AM' && hour === 12) hour = 0; return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; }; // Reset form when dialog opens or taskId changes, or populate with edit data useEffect(() => { if (isOpen) { if (editMode && timeLogToEdit) { // Add debug logging console.log('Editing time log:', timeLogToEdit); console.log('Original time formats:', { startTime: timeLogToEdit.startTime, endTime: timeLogToEdit.endTime, hasAMPM: timeLogToEdit.startTime.includes(' ') }); // Populate form with data from the time log being edited setTimeLogId(timeLogToEdit.id); setWorkDate(new Date(timeLogToEdit.workDate)); // Fix for start and end time format issues: // Check if time already has AM/PM format, if not, convert from 24-hour format if (timeLogToEdit.startTime && !timeLogToEdit.startTime.includes(' ')) { // Convert from 24-hour format to AM/PM const convertFrom24Hour = (time24: string): string => { const [hour, minute] = time24.split(':').map(Number); const period = hour >= 12 ? 'PM' : 'AM'; const hour12 = hour % 12 || 12; // Convert 0 to 12 for 12 AM return `${hour12}:${minute.toString().padStart(2, '0')} ${period}`; }; setStartTime(convertFrom24Hour(timeLogToEdit.startTime)); setEndTime(convertFrom24Hour(timeLogToEdit.endTime)); } else { setStartTime(timeLogToEdit.startTime); setEndTime(timeLogToEdit.endTime); } setTimeLogHours(timeLogToEdit.hrsSpent.toString()); setRemainingHours(timeLogToEdit.remainingHr.toString()); // Ensure description is properly formatted for the rich text editor setTimeLogDescription(timeLogToEdit.comments || ""); setSelectedTask(timeLogToEdit.taskId); setDescriptionError(false); } else { // Reset form for new time log - default start time to 9:30 AM setTimeLogId(null); setWorkDate(new Date()); setStartTime("9:30 AM"); setEndTime("10:00 AM"); // Calculate hours spent setTimeLogHours(calculateHoursSpent("9:30 AM", "10:00 AM")); setTimeLogDescription(""); setSelectedTask(taskId || null); setDescriptionError(false); // If taskId is provided, also fetch the task to get the remaining hours if (taskId) { tasksApi.getById(taskId).then(task => { if (task?.remainingHr) { setRemainingHours(task.remainingHr.toString()); } else { setRemainingHours(""); } }).catch(error => { console.error("Error fetching task details:", error); setRemainingHours(""); }); } else { setRemainingHours(""); } } } }, [isOpen, taskId, editMode, timeLogToEdit]); // Fetch tasks for dropdown const { data: tasks = [], isLoading: isLoadingTasks, error: tasksError } = useQuery({ queryKey: ["tasks"], queryFn: () => tasksApi.getAll(), }); // Create task dropdown options const taskOptions = tasks.map(task => ({ label: `${task.title} (ID: ${task.id})`, value: task.id.toString() })); // Create mutation const createMutation = useMutation({ mutationFn: () => { // Convert AM/PM time to 24-hour format for API const startTime24 = convertTo24HourFormat(startTime); const endTime24 = convertTo24HourFormat(endTime); return timeLogApi.create({ workDate: format(workDate, "yyyy-MM-dd"), startTime: startTime24, // Using 24-hour format endTime: endTime24, // Using 24-hour format comments: timeLogDescription, // Rich text HTML content createdBy: userData?.userId ? `${userData.userId}` : "", updatedBy: userData?.userId ? `${userData.userId}` : "", taskId: selectedTask || 0, hrsSpent: parseFloat(timeLogHours) || 0, remainingHr: parseFloat(remainingHours) || 0, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["timeLogs"] }); if (taskId) { queryClient.invalidateQueries({ queryKey: ["tasks", taskId] }); } showToast("success", "Time Log Created", "The time log was created successfully."); handleClose(); if (onTimeLogAdded) { onTimeLogAdded(); } }, onError: (error) => { showToast("error", "Error", `Failed to create time log: ${error}`); }, onSettled: () => { // Reset submitting state regardless of success or error setIsSubmitting(false); }, }); // Update mutation const updateMutation = useMutation({ mutationFn: () => { if (!timeLogId) throw new Error("No time log ID for update"); // Convert AM/PM time to 24-hour format for API const startTime24 = convertTo24HourFormat(startTime); const endTime24 = convertTo24HourFormat(endTime); return timeLogApi.update(timeLogId, { id: timeLogId, workDate: format(workDate, "yyyy-MM-dd"), startTime: startTime24, // Using 24-hour format endTime: endTime24, // Using 24-hour format comments: timeLogDescription, // Rich text HTML content createdBy: timeLogToEdit?.createdBy || (userData?.userId ? `${userData.userId}` : ""), updatedBy: userData?.userId ? `${userData.userId}` : "", taskId: selectedTask || 0, hrsSpent: parseFloat(timeLogHours) || 0, remainingHr: parseFloat(remainingHours) || 0, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["timeLogs"] }); if (selectedTask) { queryClient.invalidateQueries({ queryKey: ["tasks", selectedTask] }); } showToast("success", "Time Log Updated", "The time log was updated successfully."); handleClose(); if (onTimeLogAdded) { onTimeLogAdded(); } }, onError: (error) => { showToast("error", "Error", `Failed to update time log: ${error}`); }, onSettled: () => { // Reset submitting state regardless of success or error setIsSubmitting(false); }, }); // Generate time options in half-hour intervals with AM/PM format const generateTimeOptions = (): { label: string; value: string }[] => { const options: { label: string; value: string }[] = []; for (let hour = 1; hour <= 12; hour++) { // AM options options.push({ label: `${hour}:00 AM`, value: `${hour}:00 AM` }); options.push({ label: `${hour}:30 AM`, value: `${hour}:30 AM` }); } for (let hour = 1; hour <= 12; hour++) { // PM options options.push({ label: `${hour}:00 PM`, value: `${hour}:00 PM` }); options.push({ label: `${hour}:30 PM`, value: `${hour}:30 PM` }); } return options; }; const timeOptions = generateTimeOptions(); // Function to get next time slot const getNextTimeSlot = (currentTime: string): string => { const timeIndex = timeOptions.findIndex(option => option.value === currentTime); if (timeIndex !== -1 && timeIndex < timeOptions.length - 1) { return timeOptions[timeIndex + 1].value; } // If current time is the last option, just return it (this is a fallback) return currentTime; }; // Initialize timeLogHours on component mount useEffect(() => { if (startTime && endTime && !timeLogHours) { setTimeLogHours(calculateHoursSpent(startTime, endTime)); } }, []); // Handle form submission const handleSubmit = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); const currentTime = Date.now(); // Prevent multiple submissions with debounce (2 seconds) if (isSubmitting || (currentTime - lastSubmissionTime < 2000)) { return; // Already submitting or too soon after last submission } // Reset validation errors setDescriptionError(false); // Validate required fields if (!selectedTask) { showToast("error", "Error", "Please select a task"); return; } if (!startTime || !endTime) { showToast("error", "Error", "Please select start and end times"); return; } // Check if the description is empty or only contains HTML tags without text const strippedDescription = timeLogDescription.replace(/<[^>]*>/g, '').trim(); if (!strippedDescription) { setDescriptionError(true); showToast("error", "Error", "Description is required"); return; } // Set submitting state setIsSubmitting(true); setLastSubmissionTime(currentTime); // If in edit mode, update the time log, otherwise create a new one if (editMode && timeLogId) { updateMutation.mutate(); } else { createMutation.mutate(); } }; // Function to get yesterday's date const getYesterday = () => { const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); return yesterday; }; // Handle clean close with reset const handleClose = () => { setWorkDate(new Date()); setStartTime("9:30 AM"); setEndTime("10:00 AM"); setTimeLogHours(calculateHoursSpent("9:30 AM", "10:00 AM")); setRemainingHours(""); setTimeLogDescription(""); setSelectedTask(null); setTimeLogId(null); setDescriptionError(false); setIsSubmitting(false); // Reset submitting state setLastSubmissionTime(0); // Reset last submission time onClose(); }; if (!isOpen) return null; // Find selected task details if available const selectedTaskDetails = tasks.find(t => t.id === selectedTask); // Determine if the current operation is in progress const isPending = isSubmitting || (editMode ? updateMutation.isPending : createMutation.isPending); // Helper function to ensure content is HTML formatted const ensureHtmlFormat = (content: string): string => { if (!content) return ''; // Check if the content already looks like HTML (contains HTML tags) const hasHtmlTags = /<[a-z][\s\S]*>/i.test(content); // If it's already HTML, return as is if (hasHtmlTags) { return content; } // Otherwise, wrap it in a paragraph tag return `

${content}

`; }; return (
{ // Prevent closing while submitting if (!isPending) { handleClose(); } }} >
e.stopPropagation()} style={{ zIndex: 10000 }} > {/* Close button in the corner */}

{editMode ? "Edit Time Log" : selectedTaskDetails ? `Log Time for ${selectedTaskDetails.title}` : "Add New Time Log"}

date && setWorkDate(date)} initialFocus disabled={(date) => { // Only allow today and yesterday in normal mode // In edit mode, allow the current date from the time log const yesterday = getYesterday(); if (editMode && timeLogToEdit) { const timeLogDate = new Date(timeLogToEdit.workDate); // Allow the original date, today and yesterday return date > new Date() || (date < yesterday && !isSameDay(date, timeLogDate)); } return date > new Date() || (date < yesterday); }} />
{(!taskId || !selectedTaskDetails) && (
{isLoadingTasks ? (
Loading tasks...
) : ( { const taskId = parseInt(value); setSelectedTask(taskId); // Auto-fill remaining hours from the task if available const task = tasks.find(t => t.id === taskId); if (task?.remainingHr) { setRemainingHours(task.remainingHr.toString()); } }} placeholder="Select a task" className="w-full" disabled={!!taskId || (editMode && !!timeLogToEdit)} /> )}
)}
{ setStartTime(value); // Automatically set end time to next time slot const nextSlot = getNextTimeSlot(value); setEndTime(nextSlot); // Calculate hours spent between start and end const hoursSpent = calculateHoursSpent(value, nextSlot); setTimeLogHours(hoursSpent); }} placeholder="Select start time" className="w-full" />
{ setEndTime(value); if (startTime) { // Calculate hours spent between start and end const hoursSpent = calculateHoursSpent(startTime, value); setTimeLogHours(hoursSpent); } }} placeholder="Select end time" className="w-full" />
setTimeLogHours(e.target.value)} placeholder="Hours worked" className="bg-gray-50" readOnly />

Auto-calculated from time range

setRemainingHours(e.target.value)} placeholder="Hours remaining" />
{descriptionError && (
Required field
)}
{ setTimeLogDescription(value); if (value.trim()) { setDescriptionError(false); } }} placeholder="What did you work on? Please provide detailed description of your work." minHeight="150px" className={cn( descriptionError && "border-rose-500 focus-visible:ring-rose-500" )} />

Provide a detailed description of the work completed during this time period.

); }