801 lines
28 KiB
TypeScript
801 lines
28 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import AdjustmentModal from './AdjustmentModal';
|
|
import { RootState } from '../../store';
|
|
import { useSelector } from 'react-redux';
|
|
import { DateTime } from 'luxon';
|
|
|
|
interface LeaveFormData {
|
|
fromDate: string;
|
|
toDate: string;
|
|
}
|
|
|
|
interface AdjustmentEntry {
|
|
id: number;
|
|
date: string;
|
|
loggedHours: number;
|
|
}
|
|
|
|
interface LogEntry {
|
|
id: number;
|
|
date: string;
|
|
hours: number;
|
|
task: string;
|
|
status: "Completed" | "In Progress" | "Pending";
|
|
}
|
|
|
|
interface ProjectSummaryResponse {
|
|
totalWorkedHours: string;
|
|
totalWorkedHoursDetailed: string;
|
|
totalWorkedHoursDecimal: string;
|
|
totalWorkedMinutes: string;
|
|
dedicatedHours: number;
|
|
pendingHours: string;
|
|
firstLoginTime: string;
|
|
lastLogoutTime: string;
|
|
}
|
|
|
|
const data: LogEntry[] = [
|
|
{ id: 1, date: "2025-11-01", hours: 8, task: "Frontend Development", status: "Completed" },
|
|
{ id: 2, date: "2025-11-02", hours: 6, task: "API Integration", status: "In Progress" },
|
|
{ id: 3, date: "2025-11-03", hours: 7.5, task: "UI Testing", status: "Pending" },
|
|
{ id: 4, date: "2025-11-04", hours: 9, task: "Bug Fixing", status: "Completed" },
|
|
{ id: 5, date: "2025-11-05", hours: 5.5, task: "Code Review", status: "Completed" },
|
|
];
|
|
|
|
type TabType = 'create_and_adjust_timesheet' | 'sick' | 'adjustment';
|
|
|
|
// Validation interface
|
|
interface FormErrors {
|
|
selectedProjectId?: string;
|
|
selectedDate?: string;
|
|
startTime?: string;
|
|
endTime?: string;
|
|
general?: string;
|
|
}
|
|
|
|
const LeaveAndAdjustments: React.FC = () => {
|
|
const [activeTab, setActiveTab] = useState<TabType>('create_and_adjust_timesheet');
|
|
const projects = useSelector((state: RootState) => state.projects.projectsData);
|
|
|
|
console.log("LeaveAndAdjustments", projects)
|
|
|
|
// PTO State
|
|
const [ptoLeave, setPtoLeave] = useState({
|
|
fromDate: '',
|
|
toDate: '',
|
|
});
|
|
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
|
|
|
|
// Sick Leave State
|
|
const [sickLeave, setSickLeave] = useState<LeaveFormData>({
|
|
fromDate: '',
|
|
toDate: '',
|
|
});
|
|
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
|
|
const [selectedProjectId, setSelectedProjectId] = useState("")
|
|
const [selectedDate, setSelectedDate] = useState("")
|
|
const [totalAdjustment, setTotalAdjustment] = useState("0h 0m");
|
|
const [pendingOverride, setPendingOverride] = useState<string | null>(null);
|
|
const [loadingSummary, setLoadingSummary] = useState(false);
|
|
|
|
// Form Validation State
|
|
const [formErrors, setFormErrors] = useState<FormErrors>({});
|
|
|
|
// Project Summary State
|
|
const [projectSummary, setProjectSummary] = useState<ProjectSummaryResponse | null>(null);
|
|
const [startTime, setStartTime] = useState("");
|
|
const [endTime, setEndTime] = useState("");
|
|
|
|
// Adjustment State
|
|
const [selectedPeriod, setSelectedPeriod] = useState<string>('week');
|
|
const [adjustmentData, setAdjustmentData] = useState<AdjustmentEntry[]>([
|
|
{ id: 1, date: '2025-11-01', loggedHours: 8 },
|
|
{ id: 2, date: '2025-11-02', loggedHours: 7.5 },
|
|
{ id: 3, date: '2025-11-03', loggedHours: 8 },
|
|
]);
|
|
const [editingId, setEditingId] = useState<number | null>(null);
|
|
const [editHours, setEditHours] = useState<string>('');
|
|
|
|
// Validation function
|
|
const validateLeaveForm = (data: LeaveFormData): Partial<LeaveFormData> => {
|
|
const errors: Partial<LeaveFormData> = {};
|
|
|
|
if (!data.fromDate) {
|
|
errors.fromDate = 'From date is required';
|
|
}
|
|
|
|
if (!data.toDate) {
|
|
errors.toDate = 'To date is required';
|
|
}
|
|
|
|
if (data.fromDate && data.toDate && new Date(data.fromDate) > new Date(data.toDate)) {
|
|
errors.toDate = 'To date must be after from date';
|
|
}
|
|
|
|
return errors;
|
|
};
|
|
|
|
// Function to convert UTC time to local time in HH:mm format
|
|
const convertToLocalTime = (utcTimeString: string): string => {
|
|
if (!utcTimeString) return '';
|
|
|
|
try {
|
|
// Parse the UTC time and convert to local timezone
|
|
const localTime = DateTime.fromISO(utcTimeString, { zone: 'utc' }).toLocal();
|
|
|
|
// Format to HH:mm (24-hour format)
|
|
return localTime.toFormat('HH:mm');
|
|
} catch (error) {
|
|
console.error('Error converting time:', error);
|
|
return '';
|
|
}
|
|
};
|
|
|
|
// 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;
|
|
}
|
|
|
|
setFormErrors(prev => ({
|
|
...prev,
|
|
[field]: error
|
|
}));
|
|
|
|
return !error; // Return true if valid
|
|
};
|
|
|
|
// Comprehensive form validation
|
|
const validateForm = (): boolean => {
|
|
const errors: FormErrors = {};
|
|
|
|
// Validate individual fields
|
|
errors.selectedProjectId = validateProjectId(selectedProjectId);
|
|
errors.selectedDate = validateDate(selectedDate);
|
|
errors.startTime = validateTime(startTime, 'Start time');
|
|
errors.endTime = validateTime(endTime, 'End time');
|
|
|
|
// Validate time range and dedicated hours
|
|
if (!errors.startTime && !errors.endTime) {
|
|
errors.startTime = validateTimeRange(startTime, endTime);
|
|
if (!errors.startTime) {
|
|
errors.startTime = validateDedicatedHours(startTime, endTime);
|
|
}
|
|
}
|
|
|
|
// Filter out undefined errors
|
|
const filteredErrors = Object.fromEntries(
|
|
Object.entries(errors).filter(([_, value]) => value !== undefined)
|
|
) as FormErrors;
|
|
|
|
setFormErrors(filteredErrors);
|
|
return Object.keys(filteredErrors).length === 0;
|
|
};
|
|
|
|
const handlePtoSubmit = async () => {
|
|
const errors = validateLeaveForm(ptoLeave);
|
|
|
|
if (Object.keys(errors).length > 0) {
|
|
setPtoErrors(errors);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('Submitting PTO:', ptoLeave);
|
|
alert('PTO request submitted successfully!');
|
|
|
|
setPtoLeave({ fromDate: '', toDate: '' });
|
|
setPtoErrors({});
|
|
} catch (error) {
|
|
alert('Failed to submit request. Please try again.');
|
|
console.error('Error:', error);
|
|
}
|
|
};
|
|
|
|
// Sick Leave Handlers
|
|
const handleSickChange = (field: keyof LeaveFormData, value: string) => {
|
|
setSickLeave(prev => ({ ...prev, [field]: value }));
|
|
if (sickErrors[field]) {
|
|
setSickErrors(prev => ({ ...prev, [field]: undefined }));
|
|
}
|
|
};
|
|
|
|
const handleSickSubmit = async () => {
|
|
const errors = validateLeaveForm(sickLeave);
|
|
|
|
if (Object.keys(errors).length > 0) {
|
|
setSickErrors(errors);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('Submitting Sick Leave:', sickLeave);
|
|
alert('Sick Leave request submitted successfully!');
|
|
|
|
setSickLeave({ fromDate: '', toDate: '' });
|
|
setSickErrors({});
|
|
} catch (error) {
|
|
alert('Failed to submit request. Please try again.');
|
|
console.error('Error:', error);
|
|
}
|
|
};
|
|
|
|
const [editingEntry, setEditingEntry] = useState<AdjustmentEntry | null>(null);
|
|
const [newHours, setNewHours] = useState<number>(0);
|
|
const [comment, setComment] = useState<string>("")
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
const handlePeriodChange = (value: string) => setSelectedPeriod(value);
|
|
|
|
const handleEditClick = (entry: AdjustmentEntry) => {
|
|
setEditingEntry(entry);
|
|
const modal = document.getElementById("editModal") as HTMLDialogElement;
|
|
modal?.showModal();
|
|
};
|
|
|
|
const handleCloseModal = () => {
|
|
setEditingEntry(null);
|
|
setNewHours(0)
|
|
const modal = document.getElementById("editModal") as HTMLDialogElement;
|
|
modal?.close();
|
|
};
|
|
|
|
const handleSave = () => {
|
|
console.log("Saving:", { ...editingEntry, updatedHours: newHours });
|
|
handleCloseModal();
|
|
};
|
|
|
|
const handleProjectSelect = (projectId: string) => {
|
|
console.log("handleProjectSelect", projectId)
|
|
setSelectedProjectId(projectId);
|
|
validateField('selectedProjectId', projectId);
|
|
}
|
|
|
|
const handleSelectedDate = (value: string) => {
|
|
setSelectedDate(value);
|
|
validateField('selectedDate', value);
|
|
};
|
|
|
|
const debugTimezoneInfo = (utcTimeString: string, check: string) => {
|
|
const utcTime = DateTime.fromISO(utcTimeString, { zone: 'utc' });
|
|
const localTime = utcTime.toLocal();
|
|
|
|
console.log(`-------------------------Timezone Debug Info: ${check}----------------------`);
|
|
console.log('UTC Time:', utcTime.toString());
|
|
console.log('Local Time:', localTime.toString());
|
|
console.log('System Timezone:', localTime.zoneName);
|
|
console.log('Formatted Local Time:', localTime.toFormat('HH:mm'));
|
|
};
|
|
|
|
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);
|
|
|
|
// Convert UTC times to local timezone and set them
|
|
if (res?.data?.summary?.firstLoginTime) {
|
|
const localStartTime = convertToLocalTime(res?.data?.summary?.firstLoginTime);
|
|
debugTimezoneInfo(localStartTime, "start");
|
|
setStartTime(localStartTime);
|
|
}
|
|
|
|
if (res?.data?.summary?.lastLogoutTime) {
|
|
const localEndTime = convertToLocalTime(res?.data?.summary?.lastLogoutTime);
|
|
debugTimezoneInfo(localEndTime, "end");
|
|
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(() => {
|
|
if (selectedProjectId && selectedDate) {
|
|
fetchLogs();
|
|
} else {
|
|
setProjectSummary(null);
|
|
setStartTime("");
|
|
setEndTime("");
|
|
}
|
|
}, [selectedProjectId, selectedDate]);
|
|
|
|
// Auto calculations --- derived state
|
|
useEffect(() => {
|
|
if (!projectSummary) return; // No API summary yet
|
|
if (!startTime || !endTime) return;
|
|
|
|
// Clear previous time range errors when times change
|
|
if (formErrors.startTime?.includes('time') || formErrors.startTime?.includes('dedicated')) {
|
|
setFormErrors(prev => ({
|
|
...prev,
|
|
startTime: undefined
|
|
}));
|
|
}
|
|
|
|
// 1. Validation: prevent end < start
|
|
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;
|
|
}
|
|
|
|
// 2. Calculate worked minutes from UI input
|
|
const diff = end.diff(start, ["hours", "minutes"]).toObject();
|
|
const workedHours = diff.hours || 0;
|
|
const workedMinutes = diff.minutes || 0;
|
|
|
|
const totalWorkedDecimal = workedHours + workedMinutes / 60;
|
|
|
|
// 3. Compare with server original total
|
|
const originalWorkedDecimal = Number(projectSummary.totalWorkedHoursDecimal);
|
|
const adjustment = totalWorkedDecimal - originalWorkedDecimal;
|
|
|
|
// 4. Build readable format (always positive for display)
|
|
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);
|
|
|
|
// 5. Update Pending Hours -> dedicated - new worked
|
|
const newPending = projectSummary.dedicatedHours - totalWorkedDecimal;
|
|
const newPendingRounded = newPending.toFixed(2);
|
|
|
|
setPendingOverride(newPendingRounded);
|
|
|
|
}, [startTime, endTime, projectSummary]);
|
|
|
|
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 handleSubmit = async () => {
|
|
|
|
if (isSubmitting) return;
|
|
|
|
// Clear previous errors
|
|
setFormErrors({});
|
|
|
|
// Validate all fields
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
// Final dedicated hours check
|
|
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 {
|
|
|
|
// Add await here to properly wait for the response
|
|
const res = await window.electron.createTimesheet(payload);
|
|
console.log("API Response:", res);
|
|
|
|
if (res?.success) {
|
|
console.log("Timesheet Created Successfully");
|
|
|
|
// Show success message
|
|
alert(res.message);
|
|
|
|
// Reset form or navigate away
|
|
setSelectedProjectId("");
|
|
setSelectedDate("");
|
|
setStartTime("");
|
|
setEndTime("");
|
|
setComment("");
|
|
setFormErrors({});
|
|
setTotalAdjustment("0h 0m");
|
|
|
|
} 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"
|
|
}));
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleDescription = (value: string) => {
|
|
setComment(value);
|
|
};
|
|
|
|
return (
|
|
<div className="max-h-screen">
|
|
<div className="w-full mx-auto">
|
|
|
|
{/* Header */}
|
|
<div className=" ">
|
|
<h1 className="text-mb font-bold mb-2">Leave & Adjust Timesheet</h1>
|
|
</div>
|
|
|
|
{/* General Error Alert */}
|
|
{formErrors.general && (
|
|
<div className="alert alert-error shadow-sm my-1 px-6 py-1 text-xs text-base-100">
|
|
<span>{formErrors.general}</span>
|
|
</div>
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Main Container */}
|
|
<div className="rounded-lg bg-base-100 overflow-hidden">
|
|
{/* Tabs Navigation */}
|
|
<div className="flex ">
|
|
<button
|
|
onClick={() => setActiveTab('create_and_adjust_timesheet')}
|
|
className={`flex-1 px-6 py-3 text-xs font-semibold transition-all duration-300 relative ${activeTab === 'create_and_adjust_timesheet'
|
|
? ' text-primary border-b-2 border-primary'
|
|
: ''
|
|
}`}
|
|
>
|
|
<span className="flex items-center justify-center gap-2 cursor-pointer">
|
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
Create or Adjust Timesheet
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
<div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[81vh]">
|
|
{/* PTO Tab */}
|
|
{activeTab === 'create_and_adjust_timesheet' && (
|
|
<div className="animate-fade-in">
|
|
<div className="max-w-4xl ">
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{/* Project Selection */}
|
|
<div className="form-control col-span-2">
|
|
<label className="label">
|
|
<span className="label-text font-semibold text-sm">Select project</span>
|
|
</label>
|
|
<select
|
|
className={`select select-sm ${formErrors.selectedProjectId ? 'select-error' : ''}`}
|
|
value={selectedProjectId}
|
|
onChange={(e) => handleProjectSelect(e.target.value)}
|
|
>
|
|
<option value="" disabled>
|
|
Pick a project
|
|
</option>
|
|
{projects.map((item, index) => (
|
|
<option key={item.projectId || index} value={item.projectId}>
|
|
{item.projectName}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{formErrors.selectedProjectId && (
|
|
<label className="label">
|
|
<span className="label-text-alt text-error font-medium">{formErrors.selectedProjectId}</span>
|
|
</label>
|
|
)}
|
|
</div>
|
|
|
|
{/* Date Selection */}
|
|
<div className="form-control col-span-2">
|
|
<label className="label">
|
|
<span className="label-text font-semibold text-sm">Select timesheet date</span>
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={selectedDate}
|
|
onChange={(e) => handleSelectedDate(e.target.value)}
|
|
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.selectedDate ? 'input-error' : ''}`}
|
|
/>
|
|
{formErrors.selectedDate && (
|
|
<label className="label">
|
|
<span className="label-text-alt text-error font-medium">{formErrors.selectedDate}</span>
|
|
</label>
|
|
)}
|
|
</div>
|
|
|
|
{/* Current Total Time */}
|
|
<div className="form-control">
|
|
<label className="label">
|
|
<span className="label-text font-semibold text-sm ">Current total time</span>
|
|
</label>
|
|
{loadingSummary ? (
|
|
<div className="skeleton h-9 w-full"></div>
|
|
) :
|
|
<input
|
|
type="text"
|
|
value={
|
|
projectSummary
|
|
? `${pendingOverride ? (projectSummary.dedicatedHours - Number(pendingOverride)).toFixed(2) : projectSummary.totalWorkedHoursDecimal} hrs`
|
|
: "0 hrs"
|
|
}
|
|
readOnly
|
|
className="input input-bordered input-sm w-full bg-base-200"
|
|
/>
|
|
}
|
|
</div>
|
|
|
|
{/* Current Pending Time */}
|
|
<div className="form-control">
|
|
<label className="label">
|
|
<span className="label-text font-semibold text-sm">Current pending time</span>
|
|
</label>
|
|
{loadingSummary ? (
|
|
<div className="skeleton h-9 w-full"></div>
|
|
) :
|
|
<input
|
|
type="text"
|
|
value={
|
|
projectSummary
|
|
? `${pendingOverride ?? projectSummary.pendingHours} hrs`
|
|
: "0 hrs"
|
|
}
|
|
readOnly
|
|
className="input input-bordered input-sm w-full bg-base-200"
|
|
/>
|
|
}
|
|
</div>
|
|
|
|
{/* Start Time */}
|
|
<div className="form-control">
|
|
<label className="label">
|
|
<span className="label-text font-semibold text-sm">Start time</span>
|
|
</label>
|
|
{loadingSummary ? (
|
|
<div className="skeleton h-9 w-full"></div>
|
|
) :
|
|
<input
|
|
type="time"
|
|
value={startTime}
|
|
onChange={(e) => handleStartTimeChange(e.target.value)}
|
|
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.startTime ? 'input-error' : ''}`}
|
|
/>
|
|
}
|
|
{formErrors.startTime && (
|
|
<label className="label">
|
|
<span className="label-text-alt text-error font-medium">{formErrors.startTime}</span>
|
|
</label>
|
|
)}
|
|
</div>
|
|
|
|
{/* End Time */}
|
|
<div className="form-control">
|
|
<label className="label">
|
|
<span className="label-text font-semibold text-sm ">End time</span>
|
|
</label>
|
|
{loadingSummary ? (
|
|
<div className="skeleton h-9 w-full"></div>
|
|
) :
|
|
<input
|
|
type="time"
|
|
value={endTime}
|
|
onChange={(e) => handleEndTimeChange(e.target.value)}
|
|
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.endTime ? 'input-error' : ''}`}
|
|
/>
|
|
}
|
|
{formErrors.endTime && (
|
|
<label className="label">
|
|
<span className="label-text-alt text-error font-medium">{formErrors.endTime}</span>
|
|
</label>
|
|
)}
|
|
</div>
|
|
|
|
{/* Total Time Adjustment */}
|
|
<div className="form-control col-span-2">
|
|
<label className="label">
|
|
<span className="label-text font-semibold text-sm">Total time adjustment</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Calculated automatically"
|
|
value={totalAdjustment}
|
|
readOnly
|
|
className="input input-bordered input-sm w-full bg-base-200"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div className="form-control flex flex-col gap-1">
|
|
<label className="label">
|
|
<span className="label-text font-semibold text-sm ">Description / Reason</span>
|
|
</label>
|
|
<textarea
|
|
className="textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all"
|
|
placeholder="Please enter the task name or leave notes to help us better understand your request."
|
|
value={comment}
|
|
onChange={(e) => handleDescription(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Submit Button */}
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={isSubmitting}
|
|
className={`btn btn-primary flex-1 hover:shadow-lg transition-all duration-300 ${isSubmitting ? 'loading' : ''}`}
|
|
>
|
|
{!isSubmitting && (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
)}
|
|
{isSubmitting ? 'Submitting...' : 'Submit Request'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Other tabs remain the same */}
|
|
{activeTab === 'sick' && (
|
|
<div className="animate-fade-in">
|
|
{/* Sick leave content remains the same */}
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'adjustment' && (
|
|
<div className="animate-fade-in">
|
|
{/* Adjustment content remains the same */}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>{`
|
|
@keyframes fade-in {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.animate-fade-in {
|
|
animation: fade-in 0.3s ease-out;
|
|
}
|
|
`}</style>
|
|
|
|
<AdjustmentModal
|
|
editingEntry={editingEntry}
|
|
newHours={newHours}
|
|
setNewHours={setNewHours}
|
|
handleCloseModal={handleCloseModal}
|
|
handleSave={handleSave}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LeaveAndAdjustments; |