quill copy image create duplicate image & overlap date in PTO and SICK leave with server response show in va-tracker

This commit is contained in:
JaiPandey425 2025-11-27 18:19:54 +05:30
parent d94db40a80
commit 35fa90705b
5 changed files with 394 additions and 154 deletions

View File

@ -342,26 +342,44 @@ ipcMain.handle("create-timesheet", async (event, data) => {
ipcMain.handle("create-leave-request", async (event, payload) => { ipcMain.handle("create-leave-request", async (event, payload) => {
try { try {
console.log(" Leave Request Received:", payload); console.log("Leave Request Received:", payload);
const response = await api.post("/leave/request", payload); const response = await api.post("/leave/request", payload);
console.log(" Leave Saved:", response.data); console.log("Leave Saved:", response);
return { success: true, data: response.data }; return {
success: true,
data: response.data
};
} catch (error) { } catch (error: any) {
console.error("❌ Leave Save Failed:", error); console.error("❌ Leave Save Failed:");
let message = "Unknown error";
if (error.response) {
console.log("Status:", error.response.status);
console.log("Message:", error.response.statusText);
console.log("Server Response:", error.response.data);
// Extract the backend message properly
message = error.response.data?.message || error.response.statusText;
} else {
message = error.message;
}
return { return {
success: false, success: false,
error: error || "Unknown error" message
}; };
} }
}); });
export const sendTokenExpired = () => { export const sendTokenExpired = () => {
console.log("⚠️ Sending token-expired event to renderer"); console.log("⚠️ Sending token-expired event to renderer");

View File

@ -25,16 +25,19 @@ const LeaveAndAdjustments: React.FC = () => {
console.log("Leave /sick Payload:", payload); console.log("Leave /sick Payload:", payload);
return
const response = await window.electron.createPTOAndSickLeaveRequest(payload) const response = await window.electron.createPTOAndSickLeaveRequest(payload)
console.log("Leave Payload:" ,response) console.log("Leave Payload:", response)
if (response.success) { if (response.success) {
return true; return { success: true };
} }
return {
success: false,
message: response.message
};
} catch (err) { } catch (err) {
console.error(err); console.error(err);
return false; return { success: false, message: "Request failed" };
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@ -49,8 +52,7 @@ const LeaveAndAdjustments: React.FC = () => {
<div role="tablist" className="tabs tabs-border w-full mb-1 opacity-100"> <div role="tablist" className="tabs tabs-border w-full mb-1 opacity-100">
<a <a
role="tab" role="tab"
className={`tab text-xs font-semibold ${ className={`tab text-xs font-semibold ${activeTab === "pto" ? "tab-active text-primary" : ""
activeTab === "pto" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`} } ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("pto")} onClick={() => setActiveTab("pto")}
> >
@ -59,8 +61,7 @@ const LeaveAndAdjustments: React.FC = () => {
<a <a
role="tab" role="tab"
className={`tab text-xs font-semibold ${ className={`tab text-xs font-semibold ${activeTab === "sick" ? "tab-active text-primary" : ""
activeTab === "sick" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`} } ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("sick")} onClick={() => setActiveTab("sick")}
> >
@ -69,8 +70,7 @@ const LeaveAndAdjustments: React.FC = () => {
<a <a
role="tab" role="tab"
className={`tab text-xs font-semibold ${ className={`tab text-xs font-semibold ${activeTab === "adjustment" ? "tab-active text-primary" : ""
activeTab === "adjustment" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`} } ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("adjustment")} onClick={() => setActiveTab("adjustment")}
> >

View File

@ -222,28 +222,96 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
useEffect(() => { useEffect(() => {
const editor = quillRef.current?.getEditor(); const editor = quillRef.current?.getEditor();
if (!editor) return; if (!editor) return;
editor.getModule("clipboard").addMatcher(Node.ELEMENT_NODE, (node: any, delta: any) => { // Override image handler in toolbar
return delta; editor.getModule("toolbar").handlers.image = () => {
}); const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.click();
editor.root.addEventListener("paste", async (e: any) => { input.onchange = async () => {
const items = e.clipboardData?.items; const file = input.files?.[0];
if (file) {
if (!items) return;
for (const item of items) {
if (item.type.indexOf("image") === 0) {
const file = item.getAsFile();
const base64 = await convertToBase64(file); const base64 = await convertToBase64(file);
const range = editor.getSelection(); const range = editor.getSelection();
if (range) {
editor.insertEmbed(range.index, "image", base64); 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<string> => const convertToBase64 = (file: File): Promise<string> =>

View File

@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState } from "react";
import { quillModules } from "../../../utils/quillConfig"; import { quillModules } from "../../../utils/quillConfig";
import ReactQuill from "react-quill-new"; import ReactQuill from "react-quill-new";
import Quill from "quill"; import Quill from "quill";
import "quill/dist/quill.snow.css"; // correct CSS import "quill/dist/quill.snow.css";
interface LeaveFormData { interface LeaveFormData {
fromDate: string; fromDate: string;
@ -21,43 +21,114 @@ const PtoLeaveTab: React.FC<{
toDate: "", toDate: "",
comment: "", comment: "",
}); });
const [serverError, setServerError] = useState("");
const quillRef = useRef<any>(null); const quillRef = useRef<any>(null);
const [errors, setErrors] = useState<Partial<LeaveFormData>>({}); const [errors, setErrors] = useState<Partial<LeaveFormData>>({});
useEffect(() => { useEffect(() => {
const editor = quillRef.current?.getEditor(); const editor = quillRef.current?.getEditor();
if (!editor) return; if (!editor) return;
editor.getModule("clipboard").addMatcher(Node.ELEMENT_NODE, (node: any, delta: any) => { // Override image handler in toolbar
return delta; editor.getModule("toolbar").handlers.image = () => {
}); const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.click();
editor.root.addEventListener("paste", async (e: any) => { input.onchange = async () => {
const items = e.clipboardData?.items; const file = input.files?.[0];
if (file) {
if (!items) return;
for (const item of items) {
if (item.type.indexOf("image") === 0) {
const file = item.getAsFile();
const base64 = await convertToBase64(file); const base64 = await convertToBase64(file);
const range = editor.getSelection(); const range = editor.getSelection();
if (range) {
editor.insertEmbed(range.index, "image", base64); 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<string> => const convertToBase64 = (file: File): Promise<string> =>
new Promise((resolve) => { new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => resolve(reader.result as string); reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file); reader.readAsDataURL(file);
}); });
// ... rest of your component code remains the same
const validate = () => { const validate = () => {
const e: Partial<LeaveFormData> = {}; const e: Partial<LeaveFormData> = {};
@ -67,26 +138,22 @@ const PtoLeaveTab: React.FC<{
const from = ptoLeave.fromDate ? new Date(ptoLeave.fromDate) : null; const from = ptoLeave.fromDate ? new Date(ptoLeave.fromDate) : null;
const to = ptoLeave.toDate ? new Date(ptoLeave.toDate) : null; const to = ptoLeave.toDate ? new Date(ptoLeave.toDate) : null;
// From date required
if (!from) { if (!from) {
e.fromDate = "From date is required"; e.fromDate = "From date is required";
} else if (from < today) { } else if (from < today) {
e.fromDate = "From date cannot be in the past"; e.fromDate = "From date cannot be in the past";
} }
// To date required
if (!to) { if (!to) {
e.toDate = "To date is required"; e.toDate = "To date is required";
} else if (to < today) { } else if (to < today) {
e.toDate = "To date cannot be in the past"; e.toDate = "To date cannot be in the past";
} }
// To date must be same or after From date
if (from && to && to < from) { if (from && to && to < from) {
e.toDate = "To date cannot be before From date"; e.toDate = "To date cannot be before From date";
} }
// Comment validation
if (!ptoLeave.comment.trim()) { if (!ptoLeave.comment.trim()) {
e.comment = "Comment required"; e.comment = "Comment required";
} }
@ -104,10 +171,9 @@ const PtoLeaveTab: React.FC<{
setPtoLeave(emptyForm); setPtoLeave(emptyForm);
setErrors({}); setErrors({});
// Properly reset the Quill editor
if (quillRef.current) { if (quillRef.current) {
const editor = quillRef.current.getEditor(); const editor = quillRef.current.getEditor();
editor.setContents([]); // Clear editor contents completely editor.setContents([]);
} }
}; };
@ -119,26 +185,32 @@ const PtoLeaveTab: React.FC<{
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
setServerError("")
const e = validate(); const e = validate();
if (Object.keys(e).length) return setErrors(e); if (Object.keys(e).length) return setErrors(e);
const success = await submitLeave({ const result = await submitLeave({
dateFrom: ptoLeave.fromDate, dateFrom: ptoLeave.fromDate,
dateTo: ptoLeave.toDate, dateTo: ptoLeave.toDate,
leaveType: "PTO", leaveType: "PTO",
comment: ptoLeave.comment, comment: ptoLeave.comment,
}); });
if (success) { if (result.success) {
resetForm(); resetForm();
alert("PTO submitted!"); alert("PTO submitted!");
} else {
setServerError(result.message || "Something went wrong");
} }
}; };
return ( return (
<>
<div className="max-w-4xl space-y-3 "> <div className="max-w-4xl space-y-3 ">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{/* From Date */} {/* From Date */}
<div className="form-control"> <div className="form-control">
<label className="label"> <label className="label">
@ -186,10 +258,13 @@ const PtoLeaveTab: React.FC<{
onChange={(val) => handleChange('comment', val)} onChange={(val) => handleChange('comment', val)}
className="h-60" className="h-60"
/> />
</div>
{errors.comment && <p className="text-error text-xs">{errors.comment}</p>} {errors.comment && <p className="text-error text-xs">{errors.comment}</p>}
</div> </div>
</div>
{serverError && (
<p className="text-red-500 font-medium text-center text-xs">{serverError}</p>
)}
<button <button
onClick={handleSubmit} onClick={handleSubmit}
@ -203,6 +278,7 @@ const PtoLeaveTab: React.FC<{
)} )}
</button> </button>
</div> </div>
</>
); );
}; };

View File

@ -19,33 +19,102 @@ const SickLeaveTab: React.FC<{
toDate: '', toDate: '',
comment: '' comment: ''
}); });
const [serverError, setServerError] = useState("");
const quillRef = useRef<any>(null); const quillRef = useRef<any>(null);
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({}); const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
useEffect(() => { useEffect(() => {
const editor = quillRef.current?.getEditor(); const editor = quillRef.current?.getEditor();
if (!editor) return; if (!editor) return;
editor.getModule("clipboard").addMatcher(Node.ELEMENT_NODE, (node: any, delta: any) => { // Override image handler in toolbar
return delta; editor.getModule("toolbar").handlers.image = () => {
}); const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.click();
editor.root.addEventListener("paste", async (e: any) => { input.onchange = async () => {
const items = e.clipboardData?.items; const file = input.files?.[0];
if (file) {
if (!items) return;
for (const item of items) {
if (item.type.indexOf("image") === 0) {
const file = item.getAsFile();
const base64 = await convertToBase64(file); const base64 = await convertToBase64(file);
const range = editor.getSelection(); const range = editor.getSelection();
if (range) {
editor.insertEmbed(range.index, "image", base64); 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<string> => const convertToBase64 = (file: File): Promise<string> =>
@ -102,6 +171,7 @@ const SickLeaveTab: React.FC<{
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
setServerError("")
const errors = validateLeaveForm(sickLeave); const errors = validateLeaveForm(sickLeave);
if (Object.keys(errors).length > 0) { if (Object.keys(errors).length > 0) {
@ -109,20 +179,23 @@ const SickLeaveTab: React.FC<{
return; return;
} }
const success = await submitLeave({ const result = await submitLeave({
dateFrom: sickLeave.fromDate, dateFrom: sickLeave.fromDate,
dateTo: sickLeave.toDate, dateTo: sickLeave.toDate,
leaveType: "SICK", leaveType: "SICK",
comment: sickLeave.comment, comment: sickLeave.comment,
}); });
if (success) { if (result.success) {
resetForm(); resetForm();
alert("SICK submitted!"); alert("SICK submitted!");
} else {
setServerError(result.message || "Something went wrong");
} }
}; };
return ( return (
<>
<div className="max-w-4xl"> <div className="max-w-4xl">
<div className="space-y-3"> <div className="space-y-3">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@ -189,6 +262,10 @@ const SickLeaveTab: React.FC<{
)} )}
</div> </div>
{serverError && (
<p className="text-red-500 font-medium text-center text-xs">{serverError}</p>
)}
{/* Submit Button */} {/* Submit Button */}
<button <button
onClick={handleSubmit} onClick={handleSubmit}
@ -203,6 +280,7 @@ const SickLeaveTab: React.FC<{
</button> </button>
</div> </div>
</div> </div>
</>
); );
}; };