leave and adjustments
This commit is contained in:
parent
d33c603e6c
commit
6040fa238d
|
|
@ -16,6 +16,8 @@ app.on('ready', async () => {
|
||||||
// disables default system frame (dont do this if you want a proper working menu bar)
|
// disables default system frame (dont do this if you want a proper working menu bar)
|
||||||
frame: process.platform === 'darwin' ? false : true,
|
frame: process.platform === 'darwin' ? false : true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (isDev()) {
|
if (isDev()) {
|
||||||
mainWindow.loadURL('http://localhost:5123');
|
mainWindow.loadURL('http://localhost:5123');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,40 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from "react";
|
||||||
import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom'
|
import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom";
|
||||||
import Login from './pages/Login'
|
import Login from "./pages/Login";
|
||||||
import Dashboard from './pages/Dashboard'
|
import Dashboard from "./pages/Dashboard";
|
||||||
import Projects from './pages/Projects/Projects'
|
import Projects from "./pages/Projects/Projects";
|
||||||
import DashboardLayout from './layout/DashboardLayout'
|
import DashboardLayout from "./layout/DashboardLayout";
|
||||||
import ProtectedRoute from './routes/ProtectedRoute'
|
import ProtectedRoute from "./routes/ProtectedRoute"
|
||||||
import PublicRoute from './routes/PublicRoute'
|
import PublicRoute from "./routes/PublicRoute";
|
||||||
import { ThemeToggle } from './helper/ThemeToggle'
|
import { ThemeToggle } from "./helper/ThemeToggle"
|
||||||
|
import LeaveAndAdjustments from "./pages/LeaveAndAdjustments/LeaveAndAdjustments";
|
||||||
|
|
||||||
const App = () => {
|
// ✅ Define User type (customize based on your structure)
|
||||||
const theme = ThemeToggle()
|
interface User {
|
||||||
const [user, setUser] = useState(null)
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
const theme = ThemeToggle();
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
|
||||||
|
// ✅ Apply theme
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute('data-theme', theme)
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
}, [theme])
|
}, [theme]);
|
||||||
|
|
||||||
|
// ✅ Load user from localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedUser = localStorage.getItem('user')
|
const storedUser = localStorage.getItem("user");
|
||||||
if (storedUser) setUser(JSON.parse(storedUser))
|
if (storedUser) {
|
||||||
}, [])
|
try {
|
||||||
|
setUser(JSON.parse(storedUser));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Invalid user in localStorage", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|
@ -45,13 +60,17 @@ const App = () => {
|
||||||
>
|
>
|
||||||
<Route index element={<Dashboard />} />
|
<Route index element={<Dashboard />} />
|
||||||
<Route path="projects" element={<Projects />} />
|
<Route path="projects" element={<Projects />} />
|
||||||
|
<Route path="leave-adjustments" element={<LeaveAndAdjustments />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Default Redirect */}
|
{/* Default Redirect */}
|
||||||
<Route path="*" element={<Navigate to={user ? '/dashboard' : '/login'} />} />
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={<Navigate to={user ? "/dashboard" : "/login"} replace />}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export const ThemeToggle = () => {
|
||||||
setTheme(darkQuery.matches ? "dark" : "light");
|
setTheme(darkQuery.matches ? "dark" : "light");
|
||||||
|
|
||||||
// Listen for changes
|
// Listen for changes
|
||||||
const handleChange = (e) => {
|
const handleChange = (e: MediaQueryListEvent) => {
|
||||||
setTheme(e.matches ? "dark" : "light");
|
setTheme(e.matches ? "dark" : "light");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1,8 +1,19 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom'
|
import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { Bell, Menu, LogOut, User, Settings, Home, FolderKanban, X } from 'lucide-react'
|
import { Bell, Menu, LogOut, User, Settings, Home, FolderKanban, X, CalendarDays } from 'lucide-react'
|
||||||
|
|
||||||
const DashboardLayout = ({ user, setUser }) => {
|
|
||||||
|
interface User {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DashboardLayoutProps {
|
||||||
|
user: User | null;
|
||||||
|
setUser: React.Dispatch<React.SetStateAction<User | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
const [isSidebarOpen, setSidebarOpen] = useState(true)
|
const [isSidebarOpen, setSidebarOpen] = useState(true)
|
||||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768)
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
@ -36,6 +47,7 @@ const DashboardLayout = ({ user, setUser }) => {
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ label: 'Dashboard', path: '/dashboard', icon: <Home size={18} />, exact: true },
|
{ label: 'Dashboard', path: '/dashboard', icon: <Home size={18} />, exact: true },
|
||||||
{ label: 'Projects', path: '/dashboard/projects', icon: <FolderKanban size={18} /> },
|
{ label: 'Projects', path: '/dashboard/projects', icon: <FolderKanban size={18} /> },
|
||||||
|
{ label: 'Leave & Adjustments', path: '/dashboard/leave-adjustments', icon: <CalendarDays size={18} /> },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -44,7 +56,7 @@ const DashboardLayout = ({ user, setUser }) => {
|
||||||
<aside
|
<aside
|
||||||
className={`
|
className={`
|
||||||
fixed md:relative inset-y-0 left-0 z-40 transform py-1
|
fixed md:relative inset-y-0 left-0 z-40 transform py-1
|
||||||
bg-base-100 border-r border-base-300 transition-all duration-300 ease-in-out
|
bg-base-100 border-r border-t border-base-300 transition-all duration-300 ease-in-out
|
||||||
${isMobile
|
${isMobile
|
||||||
? isSidebarOpen
|
? isSidebarOpen
|
||||||
? 'translate-x-0 w-64'
|
? 'translate-x-0 w-64'
|
||||||
|
|
@ -111,7 +123,7 @@ const DashboardLayout = ({ user, setUser }) => {
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Navbar */}
|
{/* Navbar */}
|
||||||
<nav className="navbar bg-base-100 border-b border-base-300 p-3 sticky top-0 z-30 flex justify-between items-center">
|
<nav className="navbar bg-base-100 border-b border-t border-base-300 p-3 sticky top-0 z-30 flex justify-between items-center">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
|
|
@ -156,7 +168,7 @@ const DashboardLayout = ({ user, setUser }) => {
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Page Content */}
|
{/* Page Content */}
|
||||||
<main className="flex-1 overflow-y-auto p-4 md:p-6 no-scrollbar">
|
<main className="flex-1 overflow-y-auto p-4 md:px-6 no-scrollbar">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,530 +0,0 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
|
||||||
import {
|
|
||||||
Menu,
|
|
||||||
X,
|
|
||||||
Clock,
|
|
||||||
Activity,
|
|
||||||
TrendingUp,
|
|
||||||
Calendar,
|
|
||||||
User,
|
|
||||||
LogOut,
|
|
||||||
Settings,
|
|
||||||
Bell,
|
|
||||||
Search,
|
|
||||||
Play,
|
|
||||||
Square,
|
|
||||||
ProjectorIcon,
|
|
||||||
Projector,
|
|
||||||
ToolCase,
|
|
||||||
History,
|
|
||||||
HelpCircle,
|
|
||||||
CalendarDays
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { NavLink } from 'react-router-dom'
|
|
||||||
|
|
||||||
const projectsData = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
projectName: 'Employee Tracker',
|
|
||||||
clientName: 'TechCorp Pvt Ltd',
|
|
||||||
status: 'Active',
|
|
||||||
action: 'Start Tracking'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
projectName: 'Sales Dashboard',
|
|
||||||
clientName: 'BlueEdge Analytics',
|
|
||||||
status: 'Pending',
|
|
||||||
action: 'Start Tracking'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
projectName: 'Inventory System',
|
|
||||||
clientName: 'LogiFlow Inc',
|
|
||||||
status: 'Completed',
|
|
||||||
action: 'View Report'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
projectName: 'CRM Upgrade',
|
|
||||||
clientName: 'NextWave Solutions',
|
|
||||||
status: 'On Hold',
|
|
||||||
action: 'Resume'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
projectName: 'Mobile App Redesign',
|
|
||||||
clientName: 'PixelFusion',
|
|
||||||
status: 'Active',
|
|
||||||
action: 'Start Tracking'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
projectName: 'E-Commerce Backend',
|
|
||||||
clientName: 'ShopEasy',
|
|
||||||
status: 'Pending',
|
|
||||||
action: 'Start Tracking'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
projectName: 'Finance Tracker',
|
|
||||||
clientName: 'MoneyWise',
|
|
||||||
status: 'Active',
|
|
||||||
action: 'Start Tracking'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
projectName: 'Helpdesk Portal',
|
|
||||||
clientName: 'ServePro',
|
|
||||||
status: 'Completed',
|
|
||||||
action: 'View Report'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
projectName: 'Marketing Automation',
|
|
||||||
clientName: 'AdBoost',
|
|
||||||
status: 'Active',
|
|
||||||
action: 'Start Tracking'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
projectName: 'Data Pipeline',
|
|
||||||
clientName: 'CloudWave',
|
|
||||||
status: 'On Hold',
|
|
||||||
action: 'Resume'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
projectName: 'Support Chatbot',
|
|
||||||
clientName: 'SmartServe',
|
|
||||||
status: 'Active',
|
|
||||||
action: 'Start Tracking'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
projectName: 'Payroll System',
|
|
||||||
clientName: 'PeopleFirst HR',
|
|
||||||
status: 'Pending',
|
|
||||||
action: 'Start Tracking'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 13,
|
|
||||||
projectName: 'IoT Dashboard',
|
|
||||||
clientName: 'SensorGrid',
|
|
||||||
status: 'Completed',
|
|
||||||
action: 'View Report'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 14,
|
|
||||||
projectName: 'Security Portal',
|
|
||||||
clientName: 'SafeNet',
|
|
||||||
status: 'Active',
|
|
||||||
action: 'Start Tracking'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 15,
|
|
||||||
projectName: 'Social Media Tool',
|
|
||||||
clientName: 'Buzzify',
|
|
||||||
status: 'Pending',
|
|
||||||
action: 'Start Tracking'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 16,
|
|
||||||
projectName: 'AI Model Trainer',
|
|
||||||
clientName: 'NeuroTech',
|
|
||||||
status: 'On Hold',
|
|
||||||
action: 'Resume'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 17,
|
|
||||||
projectName: 'Project Tracker',
|
|
||||||
clientName: 'TaskFlow',
|
|
||||||
status: 'Active',
|
|
||||||
action: 'Start Tracking'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 18,
|
|
||||||
projectName: 'Booking Portal',
|
|
||||||
clientName: 'GoTravel',
|
|
||||||
status: 'Completed',
|
|
||||||
action: 'View Report'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 19,
|
|
||||||
projectName: 'Time Logger',
|
|
||||||
clientName: 'Clockify Pro',
|
|
||||||
status: 'Active',
|
|
||||||
action: 'Start Tracking'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 20,
|
|
||||||
projectName: 'Analytics Engine',
|
|
||||||
clientName: 'DataStream',
|
|
||||||
status: 'Pending',
|
|
||||||
action: 'Start Tracking'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// Sidebar Component
|
|
||||||
|
|
||||||
|
|
||||||
// Navbar Component
|
|
||||||
const Navbar = ({ toggleSidebar, user }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Navbar */}
|
|
||||||
<div className="navbar bg-base-100 px-4 sticky top-0 z-30 border-b border-base-300 py-4 ">
|
|
||||||
{/* Left side */}
|
|
||||||
<div className="flex-1 gap-2">
|
|
||||||
<button onClick={toggleSidebar} className="btn btn-ghost btn-square lg:hidden">
|
|
||||||
<Menu size={24} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right side */}
|
|
||||||
<div className="flex-none gap-2">
|
|
||||||
{/* User Dropdown */}
|
|
||||||
<div className="dropdown dropdown-end mx-3">
|
|
||||||
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
|
|
||||||
<div className="w-10 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 flex justify-center items-center">
|
|
||||||
<User size={22} />
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<ul
|
|
||||||
tabIndex={0}
|
|
||||||
className="mt-3 p-2 shadow-lg menu menu-sm dropdown-content bg-base-200 rounded-box w-64"
|
|
||||||
>
|
|
||||||
<li className="menu-title">
|
|
||||||
<div className="flex items-center gap-3 p-2">
|
|
||||||
<div className="avatar online">
|
|
||||||
<div className="w-12 rounded-full flex justify-center items-center">
|
|
||||||
<User size={25} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold">{user.name}</p>
|
|
||||||
<p className="text-xs text-base-content/60">{user.email}</p>
|
|
||||||
<div className="badge badge-success badge-xs mt-1">Online</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<div className="divider my-1"></div>
|
|
||||||
<li>
|
|
||||||
<a>
|
|
||||||
<Settings size={16} /> Settings
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<div className="divider my-1"></div>
|
|
||||||
<li>
|
|
||||||
<a className="text-error">
|
|
||||||
<LogOut size={16} /> Logout
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Widget Component
|
|
||||||
const Widget = ({ title, value, subtitle, icon, trend, color = 'primary' }) => {
|
|
||||||
return (
|
|
||||||
<div className="card bg-base-100 shadow-md hover:shadow-2xl transition-shadow">
|
|
||||||
<div className="card-body">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-sm font-medium text-base-content/60">{title}</h3>
|
|
||||||
<p className="text-3xl font-bold mt-2">{value}</p>
|
|
||||||
</div>
|
|
||||||
<div className={`p-3 bg-${color}/10 rounded-lg`}>
|
|
||||||
<div className={`text-${color}`}>{icon}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activity Timeline Component
|
|
||||||
const ActivityTimeline = () => {
|
|
||||||
const activities = [
|
|
||||||
{ time: '09:00 AM', status: 'online', label: 'Started working' },
|
|
||||||
{ time: '10:30 AM', status: 'productive', label: 'Deep work session' },
|
|
||||||
{ time: '12:00 PM', status: 'idle', label: 'Break time' },
|
|
||||||
{ time: '01:00 PM', status: 'online', label: 'Resumed work' },
|
|
||||||
{ time: '03:30 PM', status: 'productive', label: 'Meeting completed' }
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card bg-base-100 shadow-xl">
|
|
||||||
<div className="card-body">
|
|
||||||
<h2 className="card-title">Today's Activity</h2>
|
|
||||||
<div className="space-y-4 mt-4">
|
|
||||||
{activities.map((activity, index) => (
|
|
||||||
<div key={index} className="flex items-start gap-4">
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<div
|
|
||||||
className={`w-3 h-3 rounded-full ${
|
|
||||||
activity.status === 'online'
|
|
||||||
? 'bg-success'
|
|
||||||
: activity.status === 'productive'
|
|
||||||
? 'bg-primary'
|
|
||||||
: 'bg-warning'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
{index < activities.length - 1 && <div className="w-0.5 h-12 bg-base-300 my-1" />}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium">{activity.label}</p>
|
|
||||||
<p className="text-xs text-base-content/60">{activity.time}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progress Component
|
|
||||||
const ProductivityProgress = () => {
|
|
||||||
return (
|
|
||||||
<div className="card bg-base-100 shadow-xl">
|
|
||||||
<div className="card-body">
|
|
||||||
<h2 className="card-title">Productivity Breakdown</h2>
|
|
||||||
<div className="space-y-4 mt-4">
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between text-sm mb-1">
|
|
||||||
<span>Productive Time</span>
|
|
||||||
<span className="font-semibold">75%</span>
|
|
||||||
</div>
|
|
||||||
<progress className="progress progress-success" value="75" max="100"></progress>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between text-sm mb-1">
|
|
||||||
<span>Idle Time</span>
|
|
||||||
<span className="font-semibold">15%</span>
|
|
||||||
</div>
|
|
||||||
<progress className="progress progress-warning" value="15" max="100"></progress>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between text-sm mb-1">
|
|
||||||
<span>Break Time</span>
|
|
||||||
<span className="font-semibold">10%</span>
|
|
||||||
</div>
|
|
||||||
<progress className="progress progress-info" value="10" max="100"></progress>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stats stats-vertical shadow mt-4">
|
|
||||||
<div className="stat py-3">
|
|
||||||
<div className="stat-title text-xs">Efficiency Score</div>
|
|
||||||
<div className="stat-value text-2xl text-success">92%</div>
|
|
||||||
<div className="stat-desc">Above team average</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main Dashboard Component
|
|
||||||
const Dashboard = () => {
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
||||||
const [theme, setTheme] = useState('light')
|
|
||||||
const [currentTime, setCurrentTime] = useState(new Date())
|
|
||||||
|
|
||||||
const user = {
|
|
||||||
name: 'John Doe',
|
|
||||||
email: 'john@company.com',
|
|
||||||
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=John',
|
|
||||||
status: 'online'
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
||||||
const systemTheme = darkModeQuery.matches ? 'dark' : 'light'
|
|
||||||
setTheme(systemTheme)
|
|
||||||
document.documentElement.setAttribute('data-theme', systemTheme)
|
|
||||||
|
|
||||||
const handleThemeChange = (e) => {
|
|
||||||
const newTheme = e.matches ? 'dark' : 'light'
|
|
||||||
setTheme(newTheme)
|
|
||||||
document.documentElement.setAttribute('data-theme', newTheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
darkModeQuery.addEventListener('change', handleThemeChange)
|
|
||||||
|
|
||||||
// Update time every minute
|
|
||||||
const timer = setInterval(() => setCurrentTime(new Date()), 60000)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
darkModeQuery.removeEventListener('change', handleThemeChange)
|
|
||||||
clearInterval(timer)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const toggleSidebar = () => setSidebarOpen(!sidebarOpen)
|
|
||||||
|
|
||||||
const formatTime = (date) => {
|
|
||||||
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
|
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto p-4 md:p-6 no-scrollbar">
|
|
||||||
{/* Welcome Section */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Widgets Grid */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<p className="text-2xl mb-3 font-bold">Overview</p>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4 ">
|
|
||||||
<Widget
|
|
||||||
title="Logged Hours Today"
|
|
||||||
value="6h 24m"
|
|
||||||
subtitle="Today's total time"
|
|
||||||
icon={<Clock size={24} />}
|
|
||||||
trend={12}
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
<Widget
|
|
||||||
title="Logged Hours This Week"
|
|
||||||
value="4h 48m"
|
|
||||||
subtitle="75% productivity"
|
|
||||||
icon={<TrendingUp size={24} />}
|
|
||||||
trend={8}
|
|
||||||
color="success"
|
|
||||||
/>
|
|
||||||
<Widget
|
|
||||||
title="Active Project"
|
|
||||||
value="VA Tracker Project"
|
|
||||||
subtitle="Until day ends"
|
|
||||||
icon={<ProjectorIcon size={24} />}
|
|
||||||
color="warning"
|
|
||||||
/>
|
|
||||||
{/* <Widget
|
|
||||||
title="Status"
|
|
||||||
value="Online"
|
|
||||||
subtitle={`Active since ${formatTime(new Date(currentTime.getTime() - 6.4 * 60 * 60 * 1000))}`}
|
|
||||||
icon={<Activity size={24} />}
|
|
||||||
color="success"
|
|
||||||
/> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
|
||||||
<p className="text-2xl mb-3 font-bold flex justify-between">
|
|
||||||
<span>Projects</span>
|
|
||||||
<div>
|
|
||||||
<button className="ms-2 btn btn-sm btn-success btn-wide sm:w-auto rounded-full shadow-md hover:shadow-lg hover:scale-105 transition-all">
|
|
||||||
<Play className="w-5 h-5" />
|
|
||||||
Start Tracking
|
|
||||||
</button>
|
|
||||||
<button className="ms-2 btn btn-sm btn-error btn-wide sm:w-auto rounded-full shadow-md hover:shadow-lg hover:scale-105 transition-all">
|
|
||||||
<Square className="w-5 h-5" />
|
|
||||||
Stop Tracking
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
className="overflow-y-auto no-scrollbar overflow-x-auto rounded-box border border-base-content/10 bg-base-100 shadow-md"
|
|
||||||
style={{ minHeight: '100px', maxHeight: '250px' }}
|
|
||||||
>
|
|
||||||
<table className="table">
|
|
||||||
{/* Table Head */}
|
|
||||||
<thead className="sticky top-0 bg-base-200 z-10">
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Project Name</th>
|
|
||||||
<th>Client Name</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
{/* Table Body */}
|
|
||||||
<tbody>
|
|
||||||
{projectsData.map((project) => (
|
|
||||||
<tr key={project.id}>
|
|
||||||
<th>{project.id}</th>
|
|
||||||
<td>{project.projectName}</td>
|
|
||||||
<td>{project.clientName}</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
className={`badge ${
|
|
||||||
project.status === 'Active'
|
|
||||||
? 'badge-success'
|
|
||||||
: project.status === 'Pending'
|
|
||||||
? 'badge-warning'
|
|
||||||
: project.status === 'Completed'
|
|
||||||
? 'badge-info'
|
|
||||||
: 'badge-neutral'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{project.status}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
className={`badge ${
|
|
||||||
project.action === 'View Report'
|
|
||||||
? 'badge-info'
|
|
||||||
: project.action === 'Resume'
|
|
||||||
? 'badge-secondary'
|
|
||||||
: 'badge-primary'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{project.action}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<p className="text-xl font-bold mb-4">Tracking</p>
|
|
||||||
|
|
||||||
<div className="card bg-base-100 shadow-md border border-base-200 transition-all duration-300 w-full">
|
|
||||||
<div className="card-body">
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
||||||
<div className="text-center sm:text-left">
|
|
||||||
<p className="text-5xl font-extrabold text-success tracking-tight">0h 15m</p>
|
|
||||||
<p className="text-sm text-base-content/70 mt-1">Current Session</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button className="btn btn-success btn-lg rounded-xl hover:scale-105 transition-transform duration-200 w-full sm:w-auto">
|
|
||||||
Start
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divider my-4"></div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-base-content/70">Project:</span>
|
|
||||||
<span className="font-semibold text-base-content">Employee Tracker</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-base-content/70">Status:</span>
|
|
||||||
<span className="badge badge-success">Active</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Dashboard
|
|
||||||
|
|
@ -0,0 +1,343 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Clock, TrendingUp, Play, Square, ProjectorIcon } from "lucide-react";
|
||||||
|
|
||||||
|
const projectsData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
projectName: "Employee Tracker",
|
||||||
|
clientName: "TechCorp Pvt Ltd",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
projectName: "Sales Dashboard",
|
||||||
|
clientName: "BlueEdge Analytics",
|
||||||
|
status: "Pending",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
projectName: "Inventory System",
|
||||||
|
clientName: "LogiFlow Inc",
|
||||||
|
status: "Completed",
|
||||||
|
action: "View Report",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
projectName: "CRM Upgrade",
|
||||||
|
clientName: "NextWave Solutions",
|
||||||
|
status: "On Hold",
|
||||||
|
action: "Resume",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
projectName: "Mobile App Redesign",
|
||||||
|
clientName: "PixelFusion",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
projectName: "E-Commerce Backend",
|
||||||
|
clientName: "ShopEasy",
|
||||||
|
status: "Pending",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
projectName: "Finance Tracker",
|
||||||
|
clientName: "MoneyWise",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
projectName: "Helpdesk Portal",
|
||||||
|
clientName: "ServePro",
|
||||||
|
status: "Completed",
|
||||||
|
action: "View Report",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
projectName: "Marketing Automation",
|
||||||
|
clientName: "AdBoost",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
projectName: "Data Pipeline",
|
||||||
|
clientName: "CloudWave",
|
||||||
|
status: "On Hold",
|
||||||
|
action: "Resume",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
projectName: "Support Chatbot",
|
||||||
|
clientName: "SmartServe",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
projectName: "Payroll System",
|
||||||
|
clientName: "PeopleFirst HR",
|
||||||
|
status: "Pending",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
projectName: "IoT Dashboard",
|
||||||
|
clientName: "SensorGrid",
|
||||||
|
status: "Completed",
|
||||||
|
action: "View Report",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
projectName: "Security Portal",
|
||||||
|
clientName: "SafeNet",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 15,
|
||||||
|
projectName: "Social Media Tool",
|
||||||
|
clientName: "Buzzify",
|
||||||
|
status: "Pending",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 16,
|
||||||
|
projectName: "AI Model Trainer",
|
||||||
|
clientName: "NeuroTech",
|
||||||
|
status: "On Hold",
|
||||||
|
action: "Resume",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 17,
|
||||||
|
projectName: "Project Tracker",
|
||||||
|
clientName: "TaskFlow",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 18,
|
||||||
|
projectName: "Booking Portal",
|
||||||
|
clientName: "GoTravel",
|
||||||
|
status: "Completed",
|
||||||
|
action: "View Report",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 19,
|
||||||
|
projectName: "Time Logger",
|
||||||
|
clientName: "Clockify Pro",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 20,
|
||||||
|
projectName: "Analytics Engine",
|
||||||
|
clientName: "DataStream",
|
||||||
|
status: "Pending",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface WidgetProps {
|
||||||
|
title: string;
|
||||||
|
value: string | number;
|
||||||
|
subtitle?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
trend?: number;
|
||||||
|
color?: "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widget Component
|
||||||
|
const Widget: React.FC<WidgetProps> = ({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
subtitle,
|
||||||
|
icon,
|
||||||
|
trend,
|
||||||
|
color = "primary",
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className=" font-medium text-base-content/60">{title}</h3>
|
||||||
|
<p className="text-sm font-bold mt-2">{value}</p>
|
||||||
|
</div>
|
||||||
|
<div className={`p-3 bg-${color}/10 rounded-lg`}>
|
||||||
|
<div className={`text-${color}`}>{icon}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main Dashboard Component
|
||||||
|
const Dashboard = () => {
|
||||||
|
return (
|
||||||
|
<main className="flex-1 overflow-y-auto p-4 md:px-2 md:py-0.5 no-scrollbar">
|
||||||
|
{/* Welcome Section */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h1 className="text-lg font-bold">Dashboard</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Widgets Grid */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-md mb-3 font-bold">Overview</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4 ">
|
||||||
|
<Widget
|
||||||
|
title="Logged Hours Today"
|
||||||
|
value="6h 24m"
|
||||||
|
subtitle="Today's total time"
|
||||||
|
icon={<Clock size={24} />}
|
||||||
|
trend={12}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
<Widget
|
||||||
|
title="Logged Hours This Week"
|
||||||
|
value="4h 48m"
|
||||||
|
subtitle="75% productivity"
|
||||||
|
icon={<TrendingUp size={24} />}
|
||||||
|
trend={8}
|
||||||
|
color="success"
|
||||||
|
/>
|
||||||
|
<Widget
|
||||||
|
title="Active Project"
|
||||||
|
value="VA Tracker Project"
|
||||||
|
subtitle="Until day ends"
|
||||||
|
icon={<ProjectorIcon size={24} />}
|
||||||
|
color="warning"
|
||||||
|
/>
|
||||||
|
{/* <Widget
|
||||||
|
title="Status"
|
||||||
|
value="Online"
|
||||||
|
subtitle={`Active since ${formatTime(new Date(currentTime.getTime() - 6.4 * 60 * 60 * 1000))}`}
|
||||||
|
icon={<Activity size={24} />}
|
||||||
|
color="success"
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full mb-4">
|
||||||
|
<div className="text-md mb-2 font-bold flex justify-between">
|
||||||
|
<span>Tracking</span>
|
||||||
|
<div>
|
||||||
|
<button className="ms-1 btn btn-xs btn-success btn-wide sm:w-auto rounded-full shadow-md hover:shadow-lg hover:scale-105 transition-all">
|
||||||
|
<Play className="w-3 h-3" />
|
||||||
|
Start Tracking
|
||||||
|
</button>
|
||||||
|
<button className="ms-1 btn btn-xs btn-error btn-wide sm:w-auto rounded-full shadow-md hover:shadow-lg hover:scale-105 transition-all">
|
||||||
|
<Square className="w-3 h-3" />
|
||||||
|
Stop Tracking
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card bg-base-100 shadow-md border border-base-200 transition-all duration-300 w-full">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
|
<div className="text-center sm:text-left">
|
||||||
|
<p className="text-sm font-extrabold text-success tracking-tight">
|
||||||
|
0h 15m
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-base-content/70 mt-1">
|
||||||
|
Current Session
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="btn btn-success btn-sm rounded-xl hover:scale-105 transition-transform duration-200 w-full sm:w-auto">
|
||||||
|
Start
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divider my-2"></div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-base-content/70">Project:</span>
|
||||||
|
<span className="font-semibold text-base-content">
|
||||||
|
Employee Tracker
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-base-content/70">Status:</span>
|
||||||
|
<span className="badge badge-success">Active</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="">
|
||||||
|
<p className="text-md mb-2 font-bold flex">
|
||||||
|
<span>Projects</span>
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className="overflow-y-auto no-scrollbar overflow-x-auto rounded-box border border-base-content/10 bg-base-100 shadow-md"
|
||||||
|
style={{ minHeight: "100px", maxHeight: "250px" }}
|
||||||
|
>
|
||||||
|
<table className="table table-zebra w-full text-sm">
|
||||||
|
{/* Table Head */}
|
||||||
|
<thead className="sticky top-0 bg-base-200 z-10">
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Project Name</th>
|
||||||
|
<th>Client Name</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
{/* Table Body */}
|
||||||
|
<tbody>
|
||||||
|
{projectsData.map((project) => (
|
||||||
|
<tr key={project.id} className="align-middle">
|
||||||
|
<th>{project.id}</th>
|
||||||
|
<td className="whitespace-normal break-words max-w-[150px]">{project.projectName}</td>
|
||||||
|
<td className="whitespace-normal break-words max-w-[150px]">{project.clientName}</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
className={`badge badge-sm whitespace-nowrap ${project.status === "Active"
|
||||||
|
? "badge-success"
|
||||||
|
: project.status === "Pending"
|
||||||
|
? "badge-warning"
|
||||||
|
: project.status === "Completed"
|
||||||
|
? "badge-info"
|
||||||
|
: "badge-neutral"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{project.status}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
className={`badge badge-sm whitespace-nowrap ${project.action === "View Report"
|
||||||
|
? "badge-info"
|
||||||
|
: project.action === "Resume"
|
||||||
|
? "badge-secondary"
|
||||||
|
: "badge-primary"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{project.action}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
|
|
@ -0,0 +1,546 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
interface LeaveFormData {
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AdjustmentEntry {
|
||||||
|
id: number;
|
||||||
|
date: string;
|
||||||
|
loggedHours: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TabType = 'pto' | 'sick' | 'adjustment';
|
||||||
|
|
||||||
|
const LeaveAndAdjustments: React.FC = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState<TabType>('pto');
|
||||||
|
|
||||||
|
// PTO State
|
||||||
|
const [ptoLeave, setPtoLeave] = useState<LeaveFormData>({
|
||||||
|
fromDate: '',
|
||||||
|
toDate: '',
|
||||||
|
description: ''
|
||||||
|
});
|
||||||
|
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
|
||||||
|
|
||||||
|
// Sick Leave State
|
||||||
|
const [sickLeave, setSickLeave] = useState<LeaveFormData>({
|
||||||
|
fromDate: '',
|
||||||
|
toDate: '',
|
||||||
|
description: ''
|
||||||
|
});
|
||||||
|
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.description.trim()) {
|
||||||
|
errors.description = 'Description/Reason is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
// PTO Handlers
|
||||||
|
const handlePtoChange = (field: keyof LeaveFormData, value: string) => {
|
||||||
|
setPtoLeave(prev => ({ ...prev, [field]: value }));
|
||||||
|
if (ptoErrors[field]) {
|
||||||
|
setPtoErrors(prev => ({ ...prev, [field]: undefined }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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: '', description: '' });
|
||||||
|
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: '', description: '' });
|
||||||
|
setSickErrors({});
|
||||||
|
} catch (error) {
|
||||||
|
alert('Failed to submit request. Please try again.');
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adjustment Handlers
|
||||||
|
const handlePeriodChange = (period: string) => {
|
||||||
|
setSelectedPeriod(period);
|
||||||
|
if (period === 'month') {
|
||||||
|
setAdjustmentData([
|
||||||
|
{ 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 },
|
||||||
|
{ id: 4, date: '2025-11-04', loggedHours: 6 },
|
||||||
|
{ id: 5, date: '2025-11-05', loggedHours: 8 },
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setAdjustmentData([
|
||||||
|
{ 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 handleEditClick = (entry: AdjustmentEntry) => {
|
||||||
|
setEditingId(entry.id);
|
||||||
|
setEditHours(entry.loggedHours.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveEdit = async (id: number) => {
|
||||||
|
const hours = parseFloat(editHours);
|
||||||
|
|
||||||
|
if (isNaN(hours) || hours < 0 || hours > 24) {
|
||||||
|
alert('Please enter valid hours (0-24)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Updating hours for entry:', id, 'New hours:', hours);
|
||||||
|
|
||||||
|
setAdjustmentData(prev =>
|
||||||
|
prev.map(entry =>
|
||||||
|
entry.id === id ? { ...entry, loggedHours: hours } : entry
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
setEditingId(null);
|
||||||
|
setEditHours('');
|
||||||
|
alert('Hours updated successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
alert('Failed to update hours. Please try again.');
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelEdit = () => {
|
||||||
|
setEditingId(null);
|
||||||
|
setEditHours('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className=" ">
|
||||||
|
<h1 className="text-lg font-bold mb-3">Leave & Adjustments</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Container */}
|
||||||
|
<div className=" shadow-lg rounded-lg bg-base-100 overflow-hidden">
|
||||||
|
{/* Tabs Navigation */}
|
||||||
|
<div className="flex ">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('pto')}
|
||||||
|
className={`flex-1 px-6 py-4 text-sm font-semibold transition-all duration-300 relative ${
|
||||||
|
activeTab === 'pto'
|
||||||
|
? ' text-primary border-b-2 border-primary'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2 cursor-pointer">
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5" 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>
|
||||||
|
PTO Request
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('sick')}
|
||||||
|
className={`flex-1 px-6 py-4 text-sm font-semibold transition-all duration-300 relative cursor-pointer ${
|
||||||
|
activeTab === 'sick'
|
||||||
|
? ' text-primary border-b-2 border-primary'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
Sick Leave
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('adjustment')}
|
||||||
|
className={`flex-1 px-6 py-4 text-sm 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-5 w-5" 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 */}
|
||||||
|
<div className="px-8 py-4">
|
||||||
|
{/* PTO Tab */}
|
||||||
|
{activeTab === 'pto' && (
|
||||||
|
<div className="animate-fade-in">
|
||||||
|
<div className="max-w-2xl ">
|
||||||
|
<div className="mb-3">
|
||||||
|
<h2 className="text-sm font-bold mb-2">Paid Time Off Request</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-5">
|
||||||
|
<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={ptoLeave.fromDate}
|
||||||
|
onChange={(e) => handlePtoChange('fromDate', e.target.value)}
|
||||||
|
className={`input input-bordered w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`}
|
||||||
|
/>
|
||||||
|
{ptoErrors.fromDate && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt text-error font-medium">{ptoErrors.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={ptoLeave.toDate}
|
||||||
|
onChange={(e) => handlePtoChange('toDate', e.target.value)}
|
||||||
|
className={`input input-bordered w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
|
||||||
|
/>
|
||||||
|
{ptoErrors.toDate && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt text-error font-medium">{ptoErrors.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={ptoLeave.description}
|
||||||
|
onChange={(e) => handlePtoChange('description', e.target.value)}
|
||||||
|
className={`textarea textarea-bordered h-32 w-full focus:textarea-primary transition-all ${ptoErrors.description ? 'textarea-error' : ''}`}
|
||||||
|
placeholder="Please provide a reason for your PTO request..."
|
||||||
|
/>
|
||||||
|
{ptoErrors.description && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt text-error font-medium">{ptoErrors.description}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={handlePtoSubmit}
|
||||||
|
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={() => {
|
||||||
|
setPtoLeave({ fromDate: '', toDate: '', description: '' });
|
||||||
|
setPtoErrors({});
|
||||||
|
}}
|
||||||
|
className="btn btn-secondary transition-all"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sick Leave Tab */}
|
||||||
|
{activeTab === 'sick' && (
|
||||||
|
<div className="animate-fade-in">
|
||||||
|
<div className="max-w-2xl ">
|
||||||
|
<div className="mb-3">
|
||||||
|
<h2 className="text-sm font-bold mb-2">Sick Leave Request</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-5">
|
||||||
|
<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-32 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 pt-4">
|
||||||
|
<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-ghost hover:bg-slate-100 transition-all"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Adjustment Submissions Tab */}
|
||||||
|
{activeTab === 'adjustment' && (
|
||||||
|
<div className="animate-fade-in">
|
||||||
|
<div className="mb-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-md font-bold mb-2">Time Adjustments</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
{editingId === entry.id ? (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editHours}
|
||||||
|
onChange={(e) => setEditHours(e.target.value)}
|
||||||
|
className="input input-bordered input-sm w-28 focus:input-primary"
|
||||||
|
min="0"
|
||||||
|
max="24"
|
||||||
|
step="0.5"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="badge badge-lg badge-ghost font-semibold">
|
||||||
|
{entry.loggedHours} hours
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="text-right">
|
||||||
|
{editingId === entry.id ? (
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<button
|
||||||
|
// onClick={() => handleSaveEdit(entry.id)}
|
||||||
|
className="btn btn-success 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="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelEdit}
|
||||||
|
className="btn btn-ghost btn-sm hover:bg-slate-100 transition-all"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LeaveAndAdjustments;
|
||||||
|
|
@ -1,35 +1,42 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef, FormEvent, ChangeEvent } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
const Login = ({ setUser }) => {
|
interface User {
|
||||||
const [formData, setFormData] = useState({ email: 'jai@gmail.com', password: '12345' })
|
email: string
|
||||||
const inputRef = useRef(null)
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginProps {
|
||||||
|
setUser: (user: User) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Login: React.FC<LoginProps> = ({ setUser }) => {
|
||||||
|
const [formData, setFormData] = useState<User>({ email: 'jai@gmail.com', password: '12345' })
|
||||||
|
const inputRef = useRef<HTMLFormElement | null>(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setFormData({ ...formData, [e.target.name]: e.target.value })
|
setFormData({ ...formData, [e.target.name]: e.target.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e: FormEvent<HTMLFormElement> | KeyboardEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const { email, password } = formData
|
const { email, password } = formData
|
||||||
|
|
||||||
if (email === 'jai@gmail.com' && password === '12345') {
|
if (email === 'jai@gmail.com' && password === '12345') {
|
||||||
// alert('✅ Login successful!')
|
|
||||||
console.log('Login successful:', formData)
|
console.log('Login successful:', formData)
|
||||||
// Redirect or further logic here
|
localStorage.setItem('user', JSON.stringify(formData))
|
||||||
localStorage.setItem("user", JSON.stringify(formData));
|
setUser(formData)
|
||||||
setUser(formData);
|
|
||||||
navigate('/')
|
navigate('/')
|
||||||
} else {
|
} else {
|
||||||
alert('❌ Invalid email or password!')
|
alert('Invalid email or password!')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⌨️ Detect Enter key press
|
// Detect Enter key press
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEnter = (e) => {
|
const handleEnter = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
handleSubmit(e)
|
handleSubmit(e)
|
||||||
}
|
}
|
||||||
|
|
@ -43,15 +50,12 @@ const Login = ({ setUser }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/10 via-base-200 to-secondary/10 p-4">
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/10 via-base-200 to-secondary/10 p-4">
|
||||||
{/* Login Card */}
|
|
||||||
<div className="card w-full max-w-md shadow-2xl bg-base-100 backdrop-blur-sm">
|
<div className="card w-full max-w-md shadow-2xl bg-base-100 backdrop-blur-sm">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{/* Avatar */}
|
|
||||||
<div className="flex justify-center items-center mb-4">
|
<div className="flex justify-center items-center mb-4">
|
||||||
<div className="avatar placeholder">
|
<div className="avatar placeholder">
|
||||||
<div className="bg-primary text-primary-content rounded-full w-16 h-16 flex items-center justify-center">
|
<div className="bg-primary text-primary-content rounded-full w-16 h-16 flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
className="w-8 h-8 stroke-current"
|
className="w-8 h-8 stroke-current"
|
||||||
|
|
@ -67,24 +71,20 @@ const Login = ({ setUser }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
|
||||||
<h2 className="text-center text-3xl font-bold mb-2">Welcome Back</h2>
|
<h2 className="text-center text-3xl font-bold mb-2">Welcome Back</h2>
|
||||||
<p className="text-center text-base-content/60 mb-6">Sign in to Employee Tracker</p>
|
<p className="text-center text-base-content/60 mb-6">Sign in to Employee Tracker</p>
|
||||||
|
|
||||||
{/* Form */}
|
|
||||||
<form
|
<form
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="flex flex-col items-center justify-center w-full"
|
className="flex flex-col items-center justify-center w-full"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{/* Email */}
|
|
||||||
<div className="form-control w-90 px-5">
|
<div className="form-control w-90 px-5">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text font-medium">Email Address</span>
|
<span className="label-text font-medium">Email Address</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="input input-bordered flex items-center gap-2">
|
<label className="input input-bordered flex items-center gap-2">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className="w-4 h-4 opacity-70"
|
className="w-4 h-4 opacity-70"
|
||||||
|
|
@ -104,14 +104,12 @@ const Login = ({ setUser }) => {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Password */}
|
|
||||||
<div className="form-control w-90 px-5 mt-4">
|
<div className="form-control w-90 px-5 mt-4">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text font-medium">Password</span>
|
<span className="label-text font-medium">Password</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="input input-bordered flex items-center gap-2">
|
<label className="input input-bordered flex items-center gap-2">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className="w-4 h-4 opacity-70"
|
className="w-4 h-4 opacity-70"
|
||||||
|
|
@ -134,7 +132,6 @@ const Login = ({ setUser }) => {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Submit Button */}
|
|
||||||
<div className="form-control w-full px-5 mt-6">
|
<div className="form-control w-full px-5 mt-6">
|
||||||
<button type="submit" className="btn btn-primary w-full">
|
<button type="submit" className="btn btn-primary w-full">
|
||||||
Sign In
|
Sign In
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
// src/routes/ProtectedRoute.jsx
|
|
||||||
import React from "react";
|
|
||||||
import { Navigate } from "react-router-dom";
|
|
||||||
|
|
||||||
const ProtectedRoute = ({ user, children }) => {
|
|
||||||
if (!user) {
|
|
||||||
return <Navigate to="/login" replace />;
|
|
||||||
}
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProtectedRoute;
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
|
||||||
|
interface ProtectedRouteProps {
|
||||||
|
user: object | null;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ user, children }) => {
|
||||||
|
if (!user) return <Navigate to="/login" replace />;
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProtectedRoute;
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
// src/routes/PublicRoute.jsx
|
|
||||||
import React from "react";
|
|
||||||
import { Navigate } from "react-router-dom";
|
|
||||||
|
|
||||||
const PublicRoute = ({ user, children }) => {
|
|
||||||
if (user) {
|
|
||||||
return <Navigate to="/dashboard" replace />;
|
|
||||||
}
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PublicRoute;
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
|
||||||
|
interface PublicRouteProps {
|
||||||
|
user: object | null;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PublicRoute: React.FC<PublicRouteProps> = ({ user, children }) => {
|
||||||
|
if (user) return <Navigate to="/dashboard" replace />;
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PublicRoute;
|
||||||
Loading…
Reference in New Issue