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:
parent
d94db40a80
commit
35fa90705b
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,9 +52,8 @@ 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")}
|
||||||
>
|
>
|
||||||
PTO Leave
|
PTO Leave
|
||||||
|
|
@ -59,9 +61,8 @@ 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")}
|
||||||
>
|
>
|
||||||
Sick Leave
|
Sick Leave
|
||||||
|
|
@ -69,9 +70,8 @@ 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")}
|
||||||
>
|
>
|
||||||
Adjustment Submission
|
Adjustment Submission
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
editor.insertEmbed(range.index, "image", base64);
|
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<string> =>
|
const convertToBase64 = (file: File): Promise<string> =>
|
||||||
|
|
@ -571,14 +639,14 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
modules={quillModules}
|
modules={quillModules}
|
||||||
onChange={handleDescription}
|
onChange={handleDescription}
|
||||||
className="h-40"
|
className="h-40"
|
||||||
// placeholder="Please enter the task name or leave notes to help us better understand your request."
|
// placeholder="Please enter the task name or leave notes to help us better understand your request."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{formErrors.comment && (
|
{formErrors.comment && (
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text-alt text-error text-xs font-medium">{formErrors.comment}</span>
|
<span className="label-text-alt text-error text-xs font-medium">{formErrors.comment}</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
editor.insertEmbed(range.index, "image", base64);
|
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<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,90 +185,100 @@ 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="grid grid-cols-2 gap-4">
|
|
||||||
|
|
||||||
{/* From Date */}
|
<div className="max-w-4xl space-y-3 ">
|
||||||
<div className="form-control">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-semibold text-sm">From Date</span>
|
|
||||||
</label>
|
{/* From Date */}
|
||||||
<input
|
<div className="form-control">
|
||||||
type="date"
|
<label className="label">
|
||||||
disabled={isSubmitting}
|
<span className="label-text font-semibold text-sm">From Date</span>
|
||||||
value={ptoLeave.fromDate}
|
</label>
|
||||||
min={new Date().toISOString().split("T")[0]}
|
<input
|
||||||
onChange={(e) => handleChange('fromDate', e.target.value)}
|
type="date"
|
||||||
className={`input input-bordered input-sm ${errors.fromDate ? "input-error" : ""
|
disabled={isSubmitting}
|
||||||
}`}
|
value={ptoLeave.fromDate}
|
||||||
/>
|
min={new Date().toISOString().split("T")[0]}
|
||||||
{errors.fromDate && <p className="text-error text-xs">{errors.fromDate}</p>}
|
onChange={(e) => handleChange('fromDate', e.target.value)}
|
||||||
|
className={`input input-bordered input-sm ${errors.fromDate ? "input-error" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.fromDate && <p className="text-error text-xs">{errors.fromDate}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* To Date */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm">To Date</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
value={ptoLeave.toDate}
|
||||||
|
min={new Date().toISOString().split("T")[0]}
|
||||||
|
onChange={(e) => handleChange('toDate', e.target.value)}
|
||||||
|
className={`input input-bordered input-sm ${errors.toDate ? "input-error" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.toDate && <p className="text-error text-xs">{errors.toDate}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comment */}
|
||||||
|
<div className="form-control mb-10 col-span-2">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm">Description</span>
|
||||||
|
</label>
|
||||||
|
<ReactQuill
|
||||||
|
ref={quillRef}
|
||||||
|
theme="snow"
|
||||||
|
value={ptoLeave.comment}
|
||||||
|
modules={quillModules}
|
||||||
|
onChange={(val) => handleChange('comment', val)}
|
||||||
|
className="h-60"
|
||||||
|
/>
|
||||||
|
{errors.comment && <p className="text-error text-xs">{errors.comment}</p>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* To Date */}
|
{serverError && (
|
||||||
<div className="form-control">
|
<p className="text-red-500 font-medium text-center text-xs">{serverError}</p>
|
||||||
<label className="label">
|
)}
|
||||||
<span className="label-text font-semibold text-sm">To Date</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
value={ptoLeave.toDate}
|
|
||||||
min={new Date().toISOString().split("T")[0]}
|
|
||||||
onChange={(e) => handleChange('toDate', e.target.value)}
|
|
||||||
className={`input input-bordered input-sm ${errors.toDate ? "input-error" : ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
{errors.toDate && <p className="text-error text-xs">{errors.toDate}</p>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Comment */}
|
<button
|
||||||
<div className="form-control mb-10 col-span-2">
|
onClick={handleSubmit}
|
||||||
<label className="label">
|
disabled={isSubmitting}
|
||||||
<span className="label-text font-semibold text-sm">Description</span>
|
className="btn btn-primary flex-1 w-full"
|
||||||
</label>
|
>
|
||||||
<ReactQuill
|
{isSubmitting ? (
|
||||||
ref={quillRef}
|
<span className="loading loading-spinner loading-sm"></span>
|
||||||
theme="snow"
|
) : (
|
||||||
value={ptoLeave.comment}
|
"Submit PTO Request"
|
||||||
modules={quillModules}
|
)}
|
||||||
onChange={(val) => handleChange('comment', val)}
|
</button>
|
||||||
className="h-60"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{errors.comment && <p className="text-error text-xs">{errors.comment}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="btn btn-primary flex-1 w-full"
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<span className="loading loading-spinner loading-sm"></span>
|
|
||||||
) : (
|
|
||||||
"Submit PTO Request"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
editor.insertEmbed(range.index, "image", base64);
|
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<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">
|
||||||
|
|
@ -182,13 +255,17 @@ const SickLeaveTab: React.FC<{
|
||||||
className="h-60"
|
className="h-60"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{sickErrors.comment && (
|
{sickErrors.comment && (
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text-alt text-xs text-error font-medium">{sickErrors.comment}</span>
|
<span className="label-text-alt text-xs text-error font-medium">{sickErrors.comment}</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
</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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue