+ {/* Description with ReactQuill */}
+
-
+ {formErrors.comment && (
+
+ )}
{/* Submit Button */}
diff --git a/src/ui/pages/LeaveAndAdjustments/tabs/PtoLeaveTab.tsx b/src/ui/pages/LeaveAndAdjustments/tabs/PtoLeaveTab.tsx
index 0885777..ba531d8 100644
--- a/src/ui/pages/LeaveAndAdjustments/tabs/PtoLeaveTab.tsx
+++ b/src/ui/pages/LeaveAndAdjustments/tabs/PtoLeaveTab.tsx
@@ -1,5 +1,9 @@
// PtoLeaveTab.tsx
-import React, { useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
+import { quillModules } from "../../../utils/quillConfig";
+import ReactQuill from "react-quill-new";
+import Quill from "quill";
+import "quill/dist/quill.snow.css";
interface LeaveFormData {
fromDate: string;
@@ -17,9 +21,114 @@ const PtoLeaveTab: React.FC<{
toDate: "",
comment: "",
});
+ const [serverError, setServerError] = useState("");
+ const quillRef = useRef
(null);
const [errors, setErrors] = useState>({});
+ 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, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result as string);
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+
+ // ... rest of your component code remains the same
const validate = () => {
const e: Partial = {};
@@ -29,26 +138,22 @@ const PtoLeaveTab: React.FC<{
const from = ptoLeave.fromDate ? new Date(ptoLeave.fromDate) : null;
const to = ptoLeave.toDate ? new Date(ptoLeave.toDate) : null;
- // From date required
if (!from) {
e.fromDate = "From date is required";
} else if (from < today) {
e.fromDate = "From date cannot be in the past";
}
- // To date required
if (!to) {
e.toDate = "To date is required";
} else if (to < today) {
e.toDate = "To date cannot be in the past";
}
- // To date must be same or after From date
if (from && to && to < from) {
e.toDate = "To date cannot be before From date";
}
- // Comment validation
if (!ptoLeave.comment.trim()) {
e.comment = "Comment required";
}
@@ -56,92 +161,125 @@ const PtoLeaveTab: React.FC<{
return e;
};
+ const resetForm = () => {
+ const emptyForm: LeaveFormData = {
+ fromDate: "",
+ toDate: "",
+ comment: ""
+ };
+
+ setPtoLeave(emptyForm);
+ setErrors({});
+
+ if (quillRef.current) {
+ const editor = quillRef.current.getEditor();
+ editor.setContents([]);
+ }
+ };
+
+ const handleChange = (field: keyof LeaveFormData, value: string) => {
+ setPtoLeave(prev => ({ ...prev, [field]: value }));
+ if (errors[field]) {
+ setErrors(prev => ({ ...prev, [field]: undefined }));
+ }
+ };
+
const handleSubmit = async () => {
+ setServerError("")
const e = validate();
if (Object.keys(e).length) return setErrors(e);
- const success = await submitLeave({
+ const result = await submitLeave({
dateFrom: ptoLeave.fromDate,
dateTo: ptoLeave.toDate,
leaveType: "PTO",
comment: ptoLeave.comment,
});
- if (success) {
- setPtoLeave({ fromDate: "", toDate: "", comment: "" });
- setErrors({});
+ if (result.success) {
+ resetForm();
alert("PTO submitted!");
+ } else {
+ setServerError(result.message || "Something went wrong");
}
};
return (
-
-
+ <>
+
+
+
- {/* From Date */}
-
-
-
setPtoLeave({ ...ptoLeave, fromDate: e.target.value })}
- className={`input input-bordered input-sm ${errors.fromDate ? "input-error" : ""
- }`}
- />
- {errors.fromDate &&
{errors.fromDate}
}
+
+ {/* From Date */}
+
+
+
handleChange('fromDate', e.target.value)}
+ className={`input input-bordered input-sm ${errors.fromDate ? "input-error" : ""
+ }`}
+ />
+ {errors.fromDate &&
{errors.fromDate}
}
+
+
+ {/* To Date */}
+
+
+
handleChange('toDate', e.target.value)}
+ className={`input input-bordered input-sm ${errors.toDate ? "input-error" : ""
+ }`}
+ />
+ {errors.toDate &&
{errors.toDate}
}
+
+
+ {/* Comment */}
+
+
+
handleChange('comment', val)}
+ className="h-60"
+ />
+ {errors.comment && {errors.comment}
}
+
- {/* To Date */}
-
-
-
setPtoLeave({ ...ptoLeave, toDate: e.target.value })}
- className={`input input-bordered input-sm ${errors.toDate ? "input-error" : ""
- }`}
- />
- {errors.toDate &&
{errors.toDate}
}
-
+ {serverError && (
+
{serverError}
+ )}
- {/* Comment */}
-
+
-
-
-
+ >
);
};
-export default PtoLeaveTab;
+export default PtoLeaveTab;
\ No newline at end of file
diff --git a/src/ui/pages/LeaveAndAdjustments/tabs/SickLeaveTab.tsx b/src/ui/pages/LeaveAndAdjustments/tabs/SickLeaveTab.tsx
index e8456be..180e6ae 100644
--- a/src/ui/pages/LeaveAndAdjustments/tabs/SickLeaveTab.tsx
+++ b/src/ui/pages/LeaveAndAdjustments/tabs/SickLeaveTab.tsx
@@ -1,4 +1,8 @@
-import React, { useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
+import { quillModules } from "../../../utils/quillConfig";
+import ReactQuill from "react-quill-new";
+import Quill from "quill";
+import "quill/dist/quill.snow.css"; // correct CSS
interface LeaveFormData {
fromDate: string;
@@ -15,8 +19,111 @@ const SickLeaveTab: React.FC<{
toDate: '',
comment: ''
});
+ const [serverError, setServerError] = useState("");
+ const quillRef = useRef
(null);
const [sickErrors, setSickErrors] = useState>({});
+ 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);
+ });
+
const validateLeaveForm = (data: LeaveFormData): Partial => {
const errors: Partial = {};
@@ -46,7 +153,25 @@ const SickLeaveTab: React.FC<{
}
};
+ const resetForm = () => {
+ const emptyForm: LeaveFormData = {
+ fromDate: "",
+ toDate: "",
+ comment: ""
+ };
+
+ setSickLeave(emptyForm);
+ setSickErrors({});
+
+ // Properly reset the Quill editor
+ if (quillRef.current) {
+ const editor = quillRef.current.getEditor();
+ editor.setContents([]); // Clear editor contents completely
+ }
+ };
+
const handleSubmit = async () => {
+ setServerError("")
const errors = validateLeaveForm(sickLeave);
if (Object.keys(errors).length > 0) {
@@ -54,24 +179,23 @@ const SickLeaveTab: React.FC<{
return;
}
- const success = await submitLeave({
+ const result = await submitLeave({
dateFrom: sickLeave.fromDate,
dateTo: sickLeave.toDate,
leaveType: "SICK",
comment: sickLeave.comment,
});
- if (success) {
- setSickLeave({ fromDate: "", toDate: "", comment: "" });
- setSickErrors({});
+ if (result.success) {
+ resetForm();
alert("SICK submitted!");
+ } else {
+ setServerError(result.message || "Something went wrong");
}
};
-
-
-
return (
+ <>
@@ -118,31 +242,35 @@ const SickLeaveTab: React.FC<{
{/* Description/Comment Field */}
-
+
-
+ {sickErrors.comment && (
+
+ )}
+ {serverError && (
+
{serverError}
+ )}
+
{/* Submit Button */}
+ >
);
};
diff --git a/src/ui/pages/Schedule/Schedule.tsx b/src/ui/pages/Schedule/Schedule.tsx
new file mode 100644
index 0000000..36fdcfb
--- /dev/null
+++ b/src/ui/pages/Schedule/Schedule.tsx
@@ -0,0 +1,100 @@
+const Schedule = () => {
+ const rows = [
+ {
+ period: "Oct 25, 2025 to Nov 07, 2025",
+ cutoff: "Nov 11, 2025 05:29:59 AM",
+ payDate: "Nov 14, 2025"
+ },
+ {
+ period: "Nov 08, 2025 to Nov 21, 2025",
+ cutoff: "Nov 25, 2025 05:29:59 AM",
+ payDate: "Nov 28, 2025"
+ },
+ {
+ period: "Nov 22, 2025 to Dec 05, 2025",
+ cutoff: "Dec 09, 2025 05:29:59 AM",
+ payDate: "Dec 12, 2025"
+ },
+ {
+ period: "Dec 06, 2025 to Dec 19, 2025",
+ cutoff: "Dec 23, 2025 05:29:59 AM",
+ payDate: "Dec 26, 2025"
+ },
+ {
+ period: "Dec 20, 2025 to Jan 02, 2026",
+ cutoff: "Jan 06, 2026 05:29:59 AM",
+ payDate: "Jan 09, 2026"
+ },
+ {
+ period: "Jan 03, 2026 to Jan 16, 2026",
+ cutoff: "Jan 20, 2026 05:29:59 AM",
+ payDate: "Jan 23, 2026"
+ },
+ {
+ period: "Jan 17, 2026 to Jan 30, 2026",
+ cutoff: "Feb 03, 2026 05:29:59 AM",
+ payDate: "Feb 06, 2026"
+ },
+ {
+ period: "Jan 31, 2026 to Feb 13, 2026",
+ cutoff: "Feb 17, 2026 05:29:59 AM",
+ payDate: "Feb 20, 2026"
+ },
+ {
+ period: "Feb 14, 2026 to Feb 27, 2026",
+ cutoff: "Mar 03, 2026 05:29:59 AM",
+ payDate: "Mar 06, 2026"
+ },
+ {
+ period: "Feb 28, 2026 to Mar 13, 2026",
+ cutoff: "Mar 17, 2026 05:29:59 AM",
+ payDate: "Mar 20, 2026"
+ }
+ ];
+
+ return (
+ <>
+
+ {/* Header */}
+
+
Schedules
+
+
+
+
+ {/* โญ FIXED: Horizontal scroll wrapper */}
+
+
+
+
+ | # |
+ Period |
+ Cutoff Time |
+ Pay Date |
+
+
+
+
+ {rows.map((row, index) => (
+
+ | {index + 1} |
+ {row.period} |
+
+ {row.cutoff}
+
+ (Asia/Calcutta)
+ |
+ {row.payDate} |
+
+ ))}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default Schedule;
diff --git a/src/ui/pages/Timesheets/ProjectLogsModal.tsx b/src/ui/pages/Timesheets/ProjectLogsModal.tsx
index 9176fa8..9724311 100644
--- a/src/ui/pages/Timesheets/ProjectLogsModal.tsx
+++ b/src/ui/pages/Timesheets/ProjectLogsModal.tsx
@@ -70,7 +70,7 @@ const ProjectLogsModal: React.FC = ({ isOpen, onClose, projectName, logs,
{tabIdx + 1} |
{tab.title}
- {tab.appName}
+ {tab.appName}
|
{new Date(tab.from).toLocaleTimeString()} {new Date(tab.to).toLocaleTimeString()} |
{tab.durationFormatted} |
diff --git a/src/ui/store/projectSlice.tsx b/src/ui/store/projectSlice.tsx
index 2d58007..51cf1cd 100644
--- a/src/ui/store/projectSlice.tsx
+++ b/src/ui/store/projectSlice.tsx
@@ -113,24 +113,25 @@ const projectsSlice = createSlice({
}
},
+ // In your projectsSlice.ts
stopTrackingReducer(state, action: PayloadAction<{ projectId: string }>) {
- const p = state.projectTrackingMap[action.payload.projectId];
- if (p) {
- p.accumulatedTime = p.elapsedTime;
- p.isTracking = false;
- p.isPaused = false;
- p.startTime = null;
- p.pauseStartTime = null;
- p.endTime = Date.now();
- }
-}
+ const p = state.projectTrackingMap[action.payload.projectId];
+ if (p) {
+ // Only update tracking status, time will be updated from server
+ p.isTracking = false;
+ p.isPaused = false;
+ p.startTime = null;
+ p.pauseStartTime = null;
+ p.endTime = Date.now();
+ }
+ }
},
});
export const { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, initializeProjectTracking,
- updateProjectTracking,startTracking , updateElapsedTime , stopTracking , stopTrackingReducer} =
+ updateProjectTracking, startTracking, updateElapsedTime, stopTracking, stopTrackingReducer } =
projectsSlice.actions;
export default projectsSlice.reducer;