This commit is contained in:
JaiPandey425 2025-11-14 19:12:12 +05:30
parent d6481a0c27
commit 8bf3343ba3
3 changed files with 126 additions and 302 deletions

18
package-lock.json generated
View File

@ -15,6 +15,7 @@
"electron-store": "^11.0.2",
"get-windows": "^9.2.3",
"lucide-react": "^0.552.0",
"luxon": "^3.7.2",
"os-utils": "^0.0.14",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -26,6 +27,7 @@
},
"devDependencies": {
"@playwright/test": "^1.47.2",
"@types/luxon": "^3.7.1",
"@types/node": "^22.5.5",
"@types/os-utils": "^0.0.4",
"@types/react": "^18.3.8",
@ -2746,6 +2748,13 @@
"@types/node": "*"
}
},
"node_modules/@types/luxon": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
"integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
@ -7656,6 +7665,15 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/luxon": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",

View File

@ -25,6 +25,7 @@
"electron-store": "^11.0.2",
"get-windows": "^9.2.3",
"lucide-react": "^0.552.0",
"luxon": "^3.7.2",
"os-utils": "^0.0.14",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -36,6 +37,7 @@
},
"devDependencies": {
"@playwright/test": "^1.47.2",
"@types/luxon": "^3.7.1",
"@types/node": "^22.5.5",
"@types/os-utils": "^0.0.4",
"@types/react": "^18.3.8",

View File

@ -2,6 +2,7 @@ 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;
@ -22,6 +23,17 @@ interface LogEntry {
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" },
@ -42,7 +54,6 @@ const LeaveAndAdjustments: React.FC = () => {
const [ptoLeave, setPtoLeave] = useState({
fromDate: '',
toDate: '',
// description:""
});
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
@ -50,12 +61,16 @@ const LeaveAndAdjustments: React.FC = () => {
const [sickLeave, setSickLeave] = useState<LeaveFormData>({
fromDate: '',
toDate: '',
});
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
const [selectedProjectId, setSelectedProjectId] = useState("")
const [selectedDate, setSelectedDate] = useState("")
// 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[]>([
@ -82,10 +97,24 @@ const LeaveAndAdjustments: React.FC = () => {
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 '';
}
};
const handlePtoSubmit = async () => {
const errors = validateLeaveForm(ptoLeave);
@ -164,13 +193,22 @@ const LeaveAndAdjustments: React.FC = () => {
}
const handleSelectedDate = (value: string) => {
// Ensure value stays as "YYYY-MM-DD"
setSelectedDate(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'));
// return localTime.toFormat('HH:mm');
};
const fetchLogs = async () => {
try {
@ -183,23 +221,44 @@ const LeaveAndAdjustments: React.FC = () => {
console.log("📥 Summary Response:", res);
// if (res.success) {
// // setProjectLogs(res.data); // <-- store logs in state
// } else {
// console.warn("⚠️ Summary API failed:", res.message);
// }
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("");
}
};
useEffect(() => {
if (selectedProjectId && selectedDate) {
fetchLogs()
fetchLogs();
} else {
setProjectSummary(null);
setStartTime("");
setEndTime("");
}
}, [selectedProjectId, selectedDate])
}, [selectedProjectId, selectedDate]);
return (
<div className="max-h-screen">
@ -228,21 +287,6 @@ const LeaveAndAdjustments: React.FC = () => {
Create or Adjust Timesheet
</span>
</button>
{/* <button
onClick={() => setActiveTab('adjustment')}
className={`flex-1 px-6 py-3 text-xs font-semibold cursor-pointer transition-all duration-300 relative ${activeTab === 'adjustment'
? ' text-primary border-b-2 border-primary'
: ''
}`}
>
<span className="flex items-center justify-center gap-2">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Adjustments
</span>
</button> */}
</div>
{/* Tab Content */}
@ -251,10 +295,6 @@ const LeaveAndAdjustments: React.FC = () => {
{activeTab === 'create_and_adjust_timesheet' && (
<div className="animate-fade-in">
<div className="max-w-4xl ">
{/* <div className="mb-1">
<h2 className="text-sm font-bold mb-2">Paid Time Off Request</h2>
</div> */}
<div className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div className="form-control col-span-2">
@ -277,7 +317,6 @@ const LeaveAndAdjustments: React.FC = () => {
))}
</select>
{ptoErrors.fromDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{ptoErrors.fromDate}</span>
@ -297,22 +336,16 @@ const LeaveAndAdjustments: React.FC = () => {
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text font-semibold text-sm ">Current total time</span>
</label>
<input
type="time"
// onChange={(e) => handlePtoChange('fromDate', e.target.value)}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`}
type="text"
value={projectSummary ? `${projectSummary.totalWorkedHoursDecimal} hrs` : "0 hrs"}
readOnly
className="input input-bordered input-sm w-full bg-base-200"
/>
{ptoErrors.fromDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{ptoErrors.fromDate}</span>
</label>
)}
</div>
<div className="form-control">
@ -320,15 +353,11 @@ const LeaveAndAdjustments: React.FC = () => {
<span className="label-text font-semibold text-sm">Current pending time</span>
</label>
<input
type="time"
// onChange={(e) => handlePtoChange('toDate', e.target.value)}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
type="text"
value={projectSummary ? `${projectSummary.pendingHours} hrs` : "0 hrs"}
readOnly
className="input input-bordered input-sm w-full bg-base-200"
/>
{ptoErrors.toDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{ptoErrors.toDate}</span>
</label>
)}
</div>
<div className="form-control">
@ -337,11 +366,9 @@ const LeaveAndAdjustments: React.FC = () => {
</label>
<input
type="time"
value={ptoLeave.fromDate}
onChange={(e) =>
setPtoLeave((prev) => ({ ...prev, fromDate: e.target.value }))
}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`}
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="input input-bordered input-sm w-full focus:input-primary transition-all"
/>
</div>
@ -351,30 +378,22 @@ const LeaveAndAdjustments: React.FC = () => {
</label>
<input
type="time"
value={ptoLeave.toDate}
onChange={(e) =>
setPtoLeave((prev) => ({ ...prev, toDate: e.target.value }))
}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="input input-bordered input-sm w-full focus:input-primary transition-all"
/>
</div>
<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="input"
// onChange={(e) => handlePtoChange('toDate', e.target.value)}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
type="text"
placeholder="Calculated automatically"
readOnly
className="input input-bordered input-sm w-full bg-base-200"
/>
{ptoErrors.toDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{ptoErrors.toDate}</span>
</label>
)}
</div>
</div>
@ -383,10 +402,7 @@ const LeaveAndAdjustments: React.FC = () => {
<span className="label-text font-semibold text-sm ">Description / Reason</span>
</label>
<textarea
// value={ptoLeave.description}
// onChange={(e) => handlePtoChange('description', e.target.value)}
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all
`}
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."
/>
</div>
@ -416,234 +432,22 @@ const LeaveAndAdjustments: React.FC = () => {
</div>
)}
{/* Sick Leave Tab */}
{/* Other tabs remain the same */}
{activeTab === 'sick' && (
<div className="animate-fade-in">
<div className="max-w-4xl ">
<div className="mb-1">
<h2 className="text-sm font-bold mb-2">Sick Leave Request</h2>
</div>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div className="form-control">
<label className="label">
<span className="label-text font-semibold ">From Date</span>
</label>
<input
type="date"
value={sickLeave.fromDate}
onChange={(e) => handleSickChange('fromDate', e.target.value)}
className={`input input-bordered w-full focus:input-primary transition-all ${sickErrors.fromDate ? 'input-error' : ''}`}
/>
{sickErrors.fromDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{sickErrors.fromDate}</span>
</label>
)}
</div>
<div className="form-control">
<label className="label">
<span className="label-text font-semibold ">To Date</span>
</label>
<input
type="date"
value={sickLeave.toDate}
onChange={(e) => handleSickChange('toDate', e.target.value)}
className={`input input-bordered w-full focus:input-primary transition-all ${sickErrors.toDate ? 'input-error' : ''}`}
/>
{sickErrors.toDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{sickErrors.toDate}</span>
</label>
)}
</div>
</div>
{/* <div className="form-control flex flex-col gap-1">
<label className="label">
<span className="label-text font-semibold ">Description / Reason</span>
</label>
<textarea
value={sickLeave.description}
onChange={(e) => handleSickChange('description', e.target.value)}
className={`textarea textarea-bordered h-20 w-full focus:textarea-primary transition-all ${sickErrors.description ? 'textarea-error' : ''}`}
placeholder="Please describe your illness or condition..."
/>
{sickErrors.description && (
<label className="label">
<span className="label-text-alt text-error font-medium">{sickErrors.description}</span>
</label>
)}
</div> */}
<div className="flex gap-3 ">
<button
onClick={handleSickSubmit}
className="btn btn-primary flex-1 hover:shadow-lg transition-all duration-300"
>
<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>
Submit Request
</button>
<button
onClick={() => {
// setSickLeave({ fromDate: '', toDate: '', description: '' });
setSickErrors({});
}}
className="btn btn-secondary transition-all"
>
Clear
</button>
</div>
</div>
</div>
{/* Sick leave content remains the same */}
</div>
)}
{/* Adjustment Submissions Tab */}
{activeTab === 'adjustment' && (
<div className="animate-fade-in">
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<h2 className="text-md font-bold mb-2">Time Adjustments</h2>
<div className="form-control w-48">
<select
value={selectedPeriod}
onChange={(e) => handlePeriodChange(e.target.value)}
className="select select-bordered focus:select-primary font-semibold"
>
<option value="week">This Week</option>
<option value="month">This Month</option>
</select>
</div>
</div>
{/* Table */}
<div className="overflow-hidden rounded-lg border border-base-content/10">
<table className="table w-full">
<thead className="bg-base-100">
<tr>
<th className="font-bold text-base">Date</th>
<th className="font-bold text-base">Logged Hours</th>
<th className="font-bold text-base text-right">Actions</th>
</tr>
</thead>
<tbody>
{adjustmentData.map((entry) => (
<tr key={entry.id} className="transition-colors">
<td className="font-medium">
{new Date(entry.date).toLocaleDateString("en-US", {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
})}
</td>
<td>
<span className="badge badge-lg badge-ghost font-semibold">
{entry.loggedHours} hrs
</span>
</td>
<td className="text-right">
<button
onClick={() => handleEditClick(entry)}
className="btn btn-primary btn-sm hover:shadow-md transition-all"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* No Data */}
{adjustmentData.length === 0 && (
<div className="text-center py-12 text-slate-500">
<svg
className="h-16 w-16 mx-auto mb-4 opacity-50"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
<p className="font-medium">No time entries found for this period</p>
</div>
)}
{/* Adjustment content remains the same */}
</div>
)}
</div>
</div>
</div>
{/* View Status */}
{/* <div className='mt-4'>
<h1 className="text-md font-bold mb-2">View Status</h1>
<div
className="overflow-y-auto rounded-box border border-base-content/5 bg-base-100 shadow-md"
style={{ minHeight: "150px", maxHeight: "300px" }}
>
<table className="table table-zebra w-full">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Date</th>
<th>Logged Hours</th>
<th>Task</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{data.map((log, index) => (
<tr key={log.id}>
<td>{index + 1}</td>
<td>{log.date}</td>
<td className="font-medium">{log.hours} hrs</td>
<td className="whitespace-normal break-words">{log.task}</td>
<td>
<div
className={`badge ${log.status === "Completed"
? "badge-success"
: log.status === "In Progress"
? "badge-info"
: "badge-warning"
}`}
>
{log.status}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div></div> */}
<style>{`
@keyframes fade-in {
from {