Spaces:
Sleeping
Sleeping
| 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<Date>(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<number | null>(taskId || null); | |
| const [timeLogId, setTimeLogId] = useState<number | null>(null); | |
| // Validation state | |
| const [descriptionError, setDescriptionError] = useState(false); | |
| // Submission prevention state | |
| const [isSubmitting, setIsSubmitting] = useState(false); | |
| const [lastSubmissionTime, setLastSubmissionTime] = useState<number>(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 `<p>${content}</p>`; | |
| }; | |
| return ( | |
| <div | |
| className="fixed inset-0 bg-black/50 flex items-center justify-center z-[9999]" | |
| onClick={() => { | |
| // Prevent closing while submitting | |
| if (!isPending) { | |
| handleClose(); | |
| } | |
| }} | |
| > | |
| <div | |
| className="bg-background rounded-lg shadow-lg w-full max-w-md p-6 relative" | |
| onClick={(e) => e.stopPropagation()} | |
| style={{ zIndex: 10000 }} | |
| > | |
| {/* Close button in the corner */} | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="absolute right-2 top-2" | |
| disabled={isPending} | |
| onClick={() => { | |
| // Prevent closing while submitting | |
| if (!isPending) { | |
| handleClose(); | |
| } | |
| }} | |
| > | |
| <X className="h-4 w-4" /> | |
| </Button> | |
| <h3 className="text-lg font-semibold mb-4"> | |
| {editMode | |
| ? "Edit Time Log" | |
| : selectedTaskDetails | |
| ? `Log Time for ${selectedTaskDetails.title}` | |
| : "Add New Time Log"} | |
| </h3> | |
| <div className="space-y-4"> | |
| <div> | |
| <Label className="text-sm font-medium mb-1 block">Work Date</Label> | |
| <div className="grid grid-cols-2 gap-3"> | |
| <div className="grid grid-cols-2 gap-2"> | |
| <Button | |
| variant={workDate && isSameDay(workDate, getYesterday()) ? "default" : "outline"} | |
| className="w-full justify-center text-xs sm:text-sm" | |
| onClick={() => { | |
| const yesterday = getYesterday(); | |
| setWorkDate(yesterday); | |
| }} | |
| type="button" | |
| > | |
| Yesterday | |
| </Button> | |
| <Button | |
| variant={workDate && isSameDay(workDate, new Date()) ? "default" : "outline"} | |
| className="w-full justify-center text-xs sm:text-sm" | |
| onClick={() => { | |
| const today = new Date(); | |
| setWorkDate(today); | |
| }} | |
| type="button" | |
| > | |
| Today | |
| </Button> | |
| </div> | |
| <Popover> | |
| <PopoverTrigger asChild> | |
| <Button | |
| variant="outline" | |
| className={cn( | |
| "w-full justify-center text-left font-normal", | |
| !workDate && "text-muted-foreground" | |
| )} | |
| > | |
| <CalendarIcon className="mr-2 h-4 w-4" /> | |
| {workDate ? format(workDate, "PPP") : <span>Pick a date</span>} | |
| </Button> | |
| </PopoverTrigger> | |
| <PopoverContent className="w-auto p-0 z-[10001]"> | |
| <Calendar | |
| mode="single" | |
| selected={workDate} | |
| onSelect={(date) => 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); | |
| }} | |
| /> | |
| </PopoverContent> | |
| </Popover> | |
| </div> | |
| </div> | |
| {(!taskId || !selectedTaskDetails) && ( | |
| <div className="space-y-2" style={{ zIndex: 50, position: 'relative' }}> | |
| <Label className="text-sm font-medium mb-1 block">Task*</Label> | |
| {isLoadingTasks ? ( | |
| <div className="flex items-center space-x-2"> | |
| <Loader2 className="h-4 w-4 animate-spin" /> | |
| <span className="text-sm text-muted-foreground">Loading tasks...</span> | |
| </div> | |
| ) : ( | |
| <Dropdown | |
| options={taskOptions} | |
| value={selectedTask?.toString() || ""} | |
| onValueChange={(value) => { | |
| 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)} | |
| /> | |
| )} | |
| </div> | |
| )} | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div className="space-y-2" style={{ zIndex: 45, position: 'relative' }}> | |
| <Label className="text-sm font-medium mb-1 block">Start Time*</Label> | |
| <Dropdown | |
| options={timeOptions} | |
| value={startTime} | |
| onValueChange={(value) => { | |
| 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" | |
| /> | |
| </div> | |
| <div className="space-y-2" style={{ zIndex: 40, position: 'relative' }}> | |
| <Label className="text-sm font-medium mb-1 block">End Time*</Label> | |
| <Dropdown | |
| options={timeOptions} | |
| value={endTime} | |
| onValueChange={(value) => { | |
| 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" | |
| /> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div className="space-y-2"> | |
| <Label className="text-sm font-medium mb-1 block">Hours Spent*</Label> | |
| <Input | |
| type="number" | |
| min="0" | |
| step="0.5" | |
| value={timeLogHours} | |
| onChange={(e) => setTimeLogHours(e.target.value)} | |
| placeholder="Hours worked" | |
| className="bg-gray-50" | |
| readOnly | |
| /> | |
| <p className="text-xs text-muted-foreground">Auto-calculated from time range</p> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label className="text-sm font-medium mb-1 block">Remaining Hours*</Label> | |
| <Input | |
| type="number" | |
| min="0" | |
| step="0.5" | |
| value={remainingHours} | |
| onChange={(e) => setRemainingHours(e.target.value)} | |
| placeholder="Hours remaining" | |
| /> | |
| </div> | |
| </div> | |
| <div className="space-y-1"> | |
| <div className="flex items-center justify-between"> | |
| <Label className="text-sm font-medium mb-1 block flex items-center"> | |
| Description* <span className="text-rose-500 ml-0.5">*</span> | |
| </Label> | |
| {descriptionError && ( | |
| <div className="text-xs font-medium text-rose-500 flex items-center"> | |
| <AlertCircle className="h-3 w-3 mr-1" /> | |
| Required field | |
| </div> | |
| )} | |
| </div> | |
| <SlateRichTextEditor | |
| key={timeLogId || 'new-time-log'} // Add key to force re-render when time log changes | |
| initialValue={ensureHtmlFormat(timeLogDescription)} | |
| onChange={(value) => { | |
| 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" | |
| )} | |
| /> | |
| <p className="text-xs text-muted-foreground"> | |
| Provide a detailed description of the work completed during this time period. | |
| </p> | |
| </div> | |
| <Button | |
| className="w-full py-2 px-4 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white disabled:opacity-50 disabled:cursor-not-allowed" | |
| onClick={handleSubmit} | |
| disabled={isPending} | |
| > | |
| {isPending ? ( | |
| <> | |
| <Loader2 className="mr-2 h-4 w-4 animate-spin" /> | |
| {editMode ? "Updating..." : "Saving..."} | |
| </> | |
| ) : editMode ? ( | |
| <> | |
| <Check className="h-4 w-4 mr-2" /> Update Time Log | |
| </> | |
| ) : ( | |
| <> | |
| <Clock className="h-4 w-4 mr-2" /> Log Time | |
| </> | |
| )} | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |