import React, { useEffect, useRef, useState } from 'react'; import { DateTime } from 'luxon'; import ReactQuill from 'react-quill-new'; import { quillModules } from '../../../utils/quillConfig'; import Quill from 'quill'; import 'quill/dist/quill.snow.css'; import { useNavigate } from 'react-router-dom'; import { useToast } from '../../../helper/ToastContext'; interface Project { projectId: string; projectName: string; status?: string | boolean; } interface ProjectSummaryResponse { totalWorkedHours: string; totalWorkedHoursDetailed: string; totalWorkedHoursDecimal: string; totalWorkedMinutes: string; dedicatedHours: number; pendingHours: string; firstLoginTime: string; lastLogoutTime: string; } interface FormErrors { selectedProjectId?: string; selectedDate?: string; startTime?: string; endTime?: string; comment?: string; general?: string; } interface AdjustmentTabProps { projects: Project[]; } const AdjustmentTab: React.FC = ({ projects }) => { const [selectedProjectId, setSelectedProjectId] = useState(""); const [selectedDate, setSelectedDate] = useState(""); const [totalAdjustment, setTotalAdjustment] = useState("0h 0m"); const [pendingOverride, setPendingOverride] = useState(null); const [loadingSummary, setLoadingSummary] = useState(false); const [formErrors, setFormErrors] = useState({}); const [projectSummary, setProjectSummary] = useState(null); const [startTime, setStartTime] = useState(""); const [endTime, setEndTime] = useState(""); const [comment, setComment] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const quillRef = useRef(null); const navigate = useNavigate() const { showToast } = useToast(); // Field validation functions const validateProjectId = (projectId: string): string | undefined => { if (!projectId) return 'Project selection is required'; return undefined; }; const validateDate = (date: string): string | undefined => { if (!date) return 'Date is required'; const selectedDateObj = new Date(date); const today = new Date(); today.setHours(0, 0, 0, 0); if (selectedDateObj > today) return 'Cannot select future dates'; return undefined; }; const validateTime = (time: string, fieldName: string): string | undefined => { if (!time) return `${fieldName} is required`; const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/; if (!timeRegex.test(time)) return 'Invalid time format'; return undefined; }; const validateTimeRange = (start: string, end: string): string | undefined => { if (!start || !end) return undefined; const startTime = DateTime.fromFormat(start, "HH:mm"); const endTime = DateTime.fromFormat(end, "HH:mm"); if (!startTime.isValid || !endTime.isValid) return 'Invalid time values'; if (endTime <= startTime) return 'End time must be after start time'; return undefined; }; const validateDedicatedHours = (start: string, end: string): string | undefined => { if (!start || !end || !projectSummary) return undefined; const startTime = DateTime.fromFormat(start, "HH:mm"); const endTime = DateTime.fromFormat(end, "HH:mm"); if (!startTime.isValid || !endTime.isValid) return undefined; const diff = endTime.diff(startTime, ["hours", "minutes"]).toObject(); const workedHours = (diff.hours || 0) + (diff.minutes || 0) / 60; if (workedHours > projectSummary.dedicatedHours) { return `Cannot exceed project's dedicated hours (${projectSummary.dedicatedHours} hrs)`; } return undefined; }; // Real-time field validation const validateField = (field: keyof FormErrors, value: string) => { let error: string | undefined; switch (field) { case 'selectedProjectId': error = validateProjectId(value); break; case 'selectedDate': error = validateDate(value); break; case 'startTime': error = validateTime(value, 'Start time'); if (!error && endTime) { error = validateTimeRange(value, endTime) || validateDedicatedHours(value, endTime); } break; case 'endTime': error = validateTime(value, 'End time'); if (!error && startTime) { error = validateTimeRange(startTime, value) || validateDedicatedHours(startTime, value); } break; case 'comment': if (!value.trim()) { error = 'Description is required'; } break; } setFormErrors(prev => ({ ...prev, [field]: error })); return !error; }; // Comprehensive form validation const validateForm = (): boolean => { const errors: FormErrors = {}; errors.selectedProjectId = validateProjectId(selectedProjectId); errors.selectedDate = validateDate(selectedDate); errors.startTime = validateTime(startTime, 'Start time'); errors.endTime = validateTime(endTime, 'End time'); if (!comment.trim()) { errors.comment = 'Description is required'; } if (!errors.startTime && !errors.endTime) { errors.startTime = validateTimeRange(startTime, endTime); if (!errors.startTime) { errors.startTime = validateDedicatedHours(startTime, endTime); } } const filteredErrors = Object.fromEntries( Object.entries(errors).filter(([_, value]) => value !== undefined) ) as FormErrors; setFormErrors(filteredErrors); return Object.keys(filteredErrors).length === 0; }; const convertToLocalTime = (utcTimeString: string): string => { if (!utcTimeString) return ''; try { const localTime = DateTime.fromISO(utcTimeString, { zone: 'utc' }).toLocal(); return localTime.toFormat('HH:mm'); } catch (error) { console.error('Error converting time:', error); return ''; } }; const fetchLogs = async () => { try { console.log("Sending =>", selectedProjectId, selectedDate); setLoadingSummary(true); const res = await window.electron.getProjectSummary( selectedProjectId, selectedDate ); console.log("📥 Summary Response:", res); if (res.success) { setProjectSummary(res?.data?.summary); if (res?.data?.summary?.firstLoginTime) { const localStartTime = convertToLocalTime(res?.data?.summary?.firstLoginTime); setStartTime(localStartTime); } if (res?.data?.summary?.lastLogoutTime) { const localEndTime = convertToLocalTime(res?.data?.summary?.lastLogoutTime); setEndTime(localEndTime); } } else { console.warn("⚠️ Summary API failed:", res.message); setProjectSummary(null); setStartTime(""); setEndTime(""); } } catch (err) { console.error("❌ Error fetching project summary:", err); setProjectSummary(null); setStartTime(""); setEndTime(""); } finally { setLoadingSummary(false); } }; useEffect(() => { const editor = quillRef.current?.getEditor(); if (!editor) return; // Override image handler in toolbar editor.getModule("toolbar").handlers.image = () => { const input = document.createElement("input"); input.setAttribute("type", "file"); input.setAttribute("accept", "image/*"); input.click(); input.onchange = async () => { const file = input.files?.[0]; if (file) { const base64 = await convertToBase64(file); const range = editor.getSelection(); if (range) { editor.insertEmbed(range.index, "image", base64); } } }; }; // COMPLETELY disable Quill's default paste behavior const disableQuillPaste = () => { // Remove all existing paste event listeners from Quill const pasteListeners = (editor.root as any).__pasteHandlers || []; pasteListeners.forEach((handler: any) => { editor.root.removeEventListener('paste', handler); }); // Override the clipboard module to do nothing const clipboard = editor.getModule('clipboard'); const originalPaste = clipboard.paste; clipboard.paste = (e: any) => { // Completely prevent Quill's default paste behavior e.preventDefault(); e.stopPropagation(); return false; }; }; disableQuillPaste(); // Our custom paste handler - this is the ONLY handler that will run const handlePaste = async (e: ClipboardEvent) => { e.preventDefault(); e.stopPropagation(); const items = e.clipboardData?.items; if (!items || items.length === 0) return; // Check for images first for (let i = 0; i < items.length; i++) { const item = items[i]; if (item.type.startsWith("image/")) { const file = item.getAsFile(); if (file) { try { const base64 = await convertToBase64(file); const range = editor.getSelection(true); if (range !== null) { // Insert the image editor.insertEmbed(range.index, "image", base64); // Move cursor after the image editor.setSelection(range.index + 1, 0); } } catch (error) { console.error("Error processing pasted image:", error); } } return; // Exit after handling the first image } } // If no images found, handle text const text = e.clipboardData?.getData('text/plain'); if (text) { const range = editor.getSelection(true); if (range !== null) { editor.insertText(range.index, text); editor.setSelection(range.index + text.length, 0); } } }; // Add our custom paste handler with high priority editor.root.addEventListener("paste", handlePaste, true); return () => { editor.root.removeEventListener("paste", handlePaste, true); }; }, []); const convertToBase64 = (file: File): Promise => new Promise((resolve) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.readAsDataURL(file); }); useEffect(() => { if (selectedProjectId && selectedDate) { setProjectSummary(null) setStartTime(""); setEndTime(""); setComment(""); setFormErrors({}); setTotalAdjustment("0h 0m"); fetchLogs(); } else { setProjectSummary(null); setStartTime(""); setEndTime(""); } }, [selectedProjectId, selectedDate]); // Auto calculations useEffect(() => { if (!projectSummary) return; if (!startTime || !endTime) return; if (formErrors.startTime?.includes('time') || formErrors.startTime?.includes('dedicated')) { setFormErrors(prev => ({ ...prev, startTime: undefined })); } const start = DateTime.fromFormat(startTime, "HH:mm"); const end = DateTime.fromFormat(endTime, "HH:mm"); if (!start.isValid || !end.isValid) return; if (end < start) { setTotalAdjustment("0h 0m"); return; } const diff = end.diff(start, ["hours", "minutes"]).toObject(); const workedHours = diff.hours || 0; const workedMinutes = diff.minutes || 0; const totalWorkedDecimal = workedHours + workedMinutes / 60; const originalWorkedDecimal = Number(projectSummary.totalWorkedHoursDecimal); const adjustment = totalWorkedDecimal - originalWorkedDecimal; const adjH = Math.floor(Math.abs(adjustment)); const adjM = Math.round((Math.abs(adjustment) % 1) * 60); const formatted = `${adjustment >= 0 ? "+" : "-"}${adjH}h ${adjM}m`; setTotalAdjustment(formatted); const newPending = projectSummary.dedicatedHours - totalWorkedDecimal; const newPendingRounded = newPending.toFixed(2); setPendingOverride(newPendingRounded); }, [startTime, endTime, projectSummary]); const handleProjectSelect = (projectId: string) => { console.log("handleProjectSelect", projectId); setSelectedProjectId(projectId); validateField('selectedProjectId', projectId); }; const handleSelectedDate = (value: string) => { setSelectedDate(value); validateField('selectedDate', value); }; const handleStartTimeChange = (value: string) => { console.log("Start changed:", value); setStartTime(value); validateField('startTime', value); }; const handleEndTimeChange = (value: string) => { console.log("End changed:", value); setEndTime(value); validateField('endTime', value); }; const handleDescription = (value: string) => { setComment(value); validateField('comment', value); }; const resetForm = () => { setSelectedProjectId(""); setSelectedDate(""); setStartTime(""); setEndTime(""); setComment(""); setFormErrors({}); setTotalAdjustment("0h 0m"); // Properly reset the Quill editor if (quillRef.current) { const editor = quillRef.current.getEditor(); editor.setContents([]); // Clear editor contents completely } }; const handleSubmit = async () => { if (isSubmitting) return; setFormErrors({}); if (!validateForm()) { return; } const dedicatedHoursError = validateDedicatedHours(startTime, endTime); if (dedicatedHoursError) { setFormErrors(prev => ({ ...prev, startTime: dedicatedHoursError })); return; } const payload = { projectId: selectedProjectId, date: selectedDate, loginTime: `${startTime}:00`, logoutTime: `${endTime}:00`, comment }; console.log("Submitting payload:", payload); setIsSubmitting(true); try { const res = await window.electron.createTimesheet(payload); console.log("API Response:", res); if (res?.success) { console.log("Timesheet Created Successfully"); resetForm(); // Show success toast instead of alert showToast("PTO Request submitted successfully!", "success"); // Navigate after a small delay setTimeout(() => { navigate('/dashboard/leave-adjustments'); }, 300); } else { setFormErrors(prev => ({ ...prev, general: res?.message || "Failed to create timesheet" })); } } catch (err: any) { console.error("Submission error:", err); setFormErrors(prev => ({ ...prev, general: err.message || "Something went wrong while saving timesheet" })); showToast(err.message || "Something went wrong while saving timesheet", "error"); } finally { setIsSubmitting(false); } }; return (
{/* General Error Alert */} {formErrors.general && (
{formErrors.general}
)}
{/* Project Selection */}
{formErrors.selectedProjectId && ( )}
{/* Date Selection */}
handleSelectedDate(e.target.value)} className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.selectedDate ? 'input-error' : ''}`} /> {formErrors.selectedDate && ( )}
{/* Current Total Time */}
{loadingSummary ? (
) : }
{/* Current Pending Time */}
{loadingSummary ? (
) : }
{/* Start Time */}
{loadingSummary ? (
) : handleStartTimeChange(e.target.value)} className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.startTime ? 'input-error' : ''}`} /> } {formErrors.startTime && ( )}
{/* End Time */}
{loadingSummary ? (
) : handleEndTimeChange(e.target.value)} className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.endTime ? 'input-error' : ''}`} /> } {formErrors.endTime && ( )}
{/* Total Time Adjustment */}
{/* Description with ReactQuill */}
{formErrors.comment && ( )} {/* Submit Button */}
); }; export default AdjustmentTab;