Autosave: 20260217-034702
This commit is contained in:
parent
71e8f75485
commit
a7b1502197
@ -10,7 +10,7 @@ import { useAppSelector } from '../stores/hooks';
|
||||
type Props = {
|
||||
menu: MenuNavBarItem[]
|
||||
className: string
|
||||
children: ReactNode
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export default function NavBar({ menu, className = '', children }: Props) {
|
||||
@ -38,20 +38,30 @@ export default function NavBar({ menu, className = '', children }: Props) {
|
||||
className={`${className} top-0 inset-x-0 fixed ${bgColor} h-14 z-30 transition-position w-screen lg:w-auto dark:bg-dark-800`}
|
||||
>
|
||||
<div className={`flex lg:items-stretch ${containerMaxW} ${isScrolled && `border-b border-pavitra-400 dark:border-dark-700`}`}>
|
||||
<div className="flex flex-1 items-stretch h-14">{children}</div>
|
||||
<div className="flex-none items-stretch flex h-14 lg:hidden">
|
||||
<NavBarItemPlain onClick={handleMenuNavBarToggleClick}>
|
||||
<BaseIcon path={isMenuNavBarActive ? mdiClose : mdiDotsVertical} size="24" />
|
||||
</NavBarItemPlain>
|
||||
|
||||
{/* Main Menu (Desktop) - Contains All Items including Profile - Centered */}
|
||||
<div className="hidden lg:flex flex-1 items-stretch justify-center h-14">
|
||||
<NavBarMenuList menu={menu} />
|
||||
</div>
|
||||
|
||||
{/* Mobile Toggle */}
|
||||
<div className="flex-none items-stretch flex h-14 lg:hidden ml-auto">
|
||||
<NavBarItemPlain onClick={handleMenuNavBarToggleClick}>
|
||||
<BaseIcon path={isMenuNavBarActive ? mdiClose : mdiDotsVertical} size="24" />
|
||||
</NavBarItemPlain>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Dropdown */}
|
||||
<div
|
||||
className={`${
|
||||
isMenuNavBarActive ? 'block' : 'hidden'
|
||||
} flex items-center max-h-screen-menu overflow-y-auto lg:overflow-visible absolute w-screen top-14 left-0 ${bgColor} shadow-lg lg:w-auto lg:flex lg:static lg:shadow-none dark:bg-dark-800`}
|
||||
} flex items-center max-h-screen-menu overflow-y-auto lg:hidden absolute w-screen top-14 left-0 ${bgColor} shadow-lg dark:bg-dark-800`}
|
||||
>
|
||||
<NavBarMenuList menu={menu} />
|
||||
<div className="w-full">
|
||||
<NavBarMenuList menu={menu} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
}
|
||||
132
frontend/src/components/StaffOffList.tsx
Normal file
132
frontend/src/components/StaffOffList.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
import CardBox from './CardBox';
|
||||
import BaseIcon from './BaseIcon';
|
||||
import { mdiArrowLeft, mdiArrowRight, mdiCalendarCheck, mdiAlertCircle, mdiDoctor } from '@mdi/js';
|
||||
|
||||
interface TimeOffRequest {
|
||||
id: string;
|
||||
leave_type: string;
|
||||
status: string;
|
||||
starts_at: string;
|
||||
ends_at: string;
|
||||
requester: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function StaffOffList() {
|
||||
const [weekStart, setWeekStart] = useState(moment().startOf('isoWeek'));
|
||||
const [requests, setRequests] = useState<TimeOffRequest[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchRequests = async () => {
|
||||
setLoading(true);
|
||||
const start = weekStart.format('YYYY-MM-DD');
|
||||
const end = weekStart.clone().endOf('isoWeek').format('YYYY-MM-DD');
|
||||
|
||||
try {
|
||||
// Backend filter: requests that overlap with the week
|
||||
// (starts_at <= endOfWeek) AND (ends_at >= startOfWeek)
|
||||
// Using existing backend range filters which support [min, max]
|
||||
// Passing null/undefined for open-ended ranges
|
||||
const params = {
|
||||
filter: JSON.stringify({
|
||||
starts_atRange: [null, end],
|
||||
ends_atRange: [start, null],
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await axios.get('/time_off_requests', { params });
|
||||
|
||||
// Filter client-side for status/type criteria (backend might return more than needed due to basic filtering)
|
||||
const filtered = response.data.rows.filter((r: any) => {
|
||||
const isApproved = r.status === 'approved';
|
||||
const isSpecialType = ['unplanned_pto', 'medical_leave'].includes(r.leave_type);
|
||||
// Include if approved OR (unplanned/medical and not rejected/cancelled)
|
||||
const isActive = !['rejected', 'cancelled'].includes(r.status);
|
||||
return (isApproved || (isSpecialType && isActive));
|
||||
});
|
||||
|
||||
setRequests(filtered);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch staff off requests', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRequests();
|
||||
}, [weekStart]);
|
||||
|
||||
const prevWeek = () => setWeekStart(weekStart.clone().subtract(1, 'weeks'));
|
||||
const nextWeek = () => setWeekStart(weekStart.clone().add(1, 'weeks'));
|
||||
|
||||
const getIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'medical_leave': return mdiDoctor;
|
||||
case 'unplanned_pto': return mdiAlertCircle;
|
||||
default: return mdiCalendarCheck;
|
||||
}
|
||||
};
|
||||
|
||||
const getColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'medical_leave': return 'text-red-500';
|
||||
case 'unplanned_pto': return 'text-orange-500';
|
||||
default: return 'text-green-500';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<CardBox className="mb-6 h-full">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-bold">Staff off this week</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button onClick={prevWeek} className="p-1 hover:bg-gray-100 rounded-full dark:hover:bg-slate-700">
|
||||
<BaseIcon path={mdiArrowLeft} size={24} />
|
||||
</button>
|
||||
<span className="font-semibold text-sm">{weekStart.format('MMM D')} - {weekStart.clone().endOf('isoWeek').format('MMM D')}</span>
|
||||
<button onClick={nextWeek} className="p-1 hover:bg-gray-100 rounded-full dark:hover:bg-slate-700">
|
||||
<BaseIcon path={mdiArrowRight} size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-4">Loading...</div>
|
||||
) : requests.length === 0 ? (
|
||||
<div className="text-center py-4 text-gray-500">No staff off this week.</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{requests.map((r) => (
|
||||
<div key={r.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg dark:bg-slate-800">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-gray-200 dark:bg-slate-700 rounded-full flex items-center justify-center overflow-hidden">
|
||||
<span className="text-gray-600 dark:text-gray-300 font-bold">
|
||||
{r.requester?.firstName?.[0]}{r.requester?.lastName?.[0]}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{r.requester?.firstName} {r.requester?.lastName}</div>
|
||||
<div className={`text-xs flex items-center ${getColor(r.leave_type)}`}>
|
||||
<BaseIcon path={getIcon(r.leave_type)} size={14} className="mr-1"/>
|
||||
<span className="capitalize">{r.leave_type.replace('_', ' ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap ml-2">
|
||||
{moment(r.starts_at).format('ddd D')} - {moment(r.ends_at).format('ddd D')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardBox>
|
||||
);
|
||||
}
|
||||
@ -4,13 +4,11 @@ import menuNavBar from '../menuNavBar'
|
||||
import NavBar from '../components/NavBar'
|
||||
import FooterBar from '../components/FooterBar'
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||
import Search from '../components/Search';
|
||||
import { useRouter } from 'next/router'
|
||||
import {findMe, logoutUser} from "../stores/authSlice";
|
||||
import axios from 'axios';
|
||||
import { mdiAlertCircle } from '@mdi/js';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import NavBarItemPlain from '../components/NavBarItemPlain';
|
||||
import moment from 'moment';
|
||||
|
||||
import {hasPermission} from "../helpers/userPermissions";
|
||||
@ -103,11 +101,7 @@ export default function LayoutAuthenticated({
|
||||
<NavBar
|
||||
menu={menuNavBar}
|
||||
className={`${lockoutBanner ? 'top-12' : ''}`}
|
||||
>
|
||||
<NavBarItemPlain useMargin>
|
||||
<Search />
|
||||
</NavBarItemPlain>
|
||||
</NavBar>
|
||||
/>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{children}
|
||||
</div>
|
||||
@ -115,4 +109,4 @@ export default function LayoutAuthenticated({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,12 @@ const menuNavBar: MenuNavBarItem[] = [
|
||||
icon: mdiViewDashboardOutline,
|
||||
label: 'Home',
|
||||
},
|
||||
{
|
||||
href: '/users/users-list',
|
||||
label: 'Users',
|
||||
icon: mdiAccountGroup,
|
||||
permissions: 'READ_USERS'
|
||||
},
|
||||
{
|
||||
label: 'PTO',
|
||||
icon: mdiCalendarClock,
|
||||
@ -119,6 +125,11 @@ const menuNavBar: MenuNavBarItem[] = [
|
||||
label: 'My Profile',
|
||||
href: '/profile',
|
||||
},
|
||||
{
|
||||
icon: mdiThemeLightDark,
|
||||
label: 'Dark Mode',
|
||||
isToggleLightDark: true,
|
||||
},
|
||||
{
|
||||
isDivider: true,
|
||||
},
|
||||
@ -129,12 +140,6 @@ const menuNavBar: MenuNavBarItem[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: mdiThemeLightDark,
|
||||
label: 'Light/Dark',
|
||||
isDesktopNoLabel: true,
|
||||
isToggleLightDark: true,
|
||||
}
|
||||
]
|
||||
|
||||
export const webPagesNavBar = [
|
||||
|
||||
@ -13,6 +13,9 @@ import { useAppSelector } from '../stores/hooks'
|
||||
import PTOStats from '../components/PTOStats'
|
||||
import moment from 'moment'
|
||||
import Link from 'next/link'
|
||||
import StaffOffList from '../components/StaffOffList'
|
||||
import Search from '../components/Search'
|
||||
import { mdiPlusBox, mdiHospitalBox, mdiAlertDecagram } from '@mdi/js'
|
||||
|
||||
const Dashboard = () => {
|
||||
const { currentUser } = useAppSelector((state) => state.auth)
|
||||
@ -20,6 +23,7 @@ const Dashboard = () => {
|
||||
const [summary, setSummary] = useState(null)
|
||||
const [approvals, setApprovals] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [greeting, setGreeting] = useState('Hello')
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
setLoading(true)
|
||||
@ -58,6 +62,13 @@ const Dashboard = () => {
|
||||
}
|
||||
}, [currentUser, selectedYear])
|
||||
|
||||
useEffect(() => {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 12) setGreeting('Good morning')
|
||||
else if (hour < 18) setGreeting('Good afternoon')
|
||||
else setGreeting('Good evening')
|
||||
}, [])
|
||||
|
||||
const handleApprove = async (taskId) => {
|
||||
try {
|
||||
await axios.put(`/approval_tasks/${taskId}/approve`);
|
||||
@ -77,18 +88,27 @@ const Dashboard = () => {
|
||||
<title>{getPageTitle('Home')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={icon.mdiHome} title="Home" main>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-500">Year:</span>
|
||||
<select
|
||||
value={selectedYear}
|
||||
onChange={(e) => setSelectedYear(parseInt(e.target.value))}
|
||||
className="pl-2 pr-10 py-1 border rounded dark:bg-dark-800 dark:border-dark-700 dark:text-white"
|
||||
>
|
||||
{years.map(y => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
<SectionTitleLineWithButton
|
||||
icon={icon.mdiHome}
|
||||
title={`${greeting}, ${currentUser?.firstName || 'User'}`}
|
||||
main
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-6">
|
||||
<Search />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-500">Year:</span>
|
||||
<select
|
||||
value={selectedYear}
|
||||
onChange={(e) => setSelectedYear(parseInt(e.target.value))}
|
||||
className="pl-2 pr-10 py-1 border rounded dark:bg-dark-800 dark:border-dark-700 dark:text-white"
|
||||
>
|
||||
{years.map(y => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
@ -102,60 +122,101 @@ const Dashboard = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{/* Action Items (Approvals) */}
|
||||
<CardBox className="flex-1" hasTable>
|
||||
{/* Action Buttons */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<CardBox className="flex flex-col items-center justify-center p-6 cursor-pointer hover:shadow-lg transition-shadow" >
|
||||
<BaseButton
|
||||
href="/time_off_requests/time_off_requests-new"
|
||||
icon={mdiPlusBox}
|
||||
label="Request Time Off"
|
||||
color="info"
|
||||
className="w-full mb-2"
|
||||
iconSize={24}
|
||||
/>
|
||||
<p className="text-gray-500 text-sm text-center">Submit a standard PTO request.</p>
|
||||
</CardBox>
|
||||
|
||||
<CardBox className="flex flex-col items-center justify-center p-6 cursor-pointer hover:shadow-lg transition-shadow">
|
||||
<BaseButton
|
||||
href="/time_off_requests/time_off_requests-new?leave_type=medical_leave"
|
||||
icon={mdiHospitalBox}
|
||||
label="Take Medical Day"
|
||||
color="danger"
|
||||
className="w-full mb-2"
|
||||
iconSize={24}
|
||||
/>
|
||||
<p className="text-gray-500 text-sm text-center">Report a sick day or medical leave.</p>
|
||||
</CardBox>
|
||||
|
||||
<CardBox className="flex flex-col items-center justify-center p-6 cursor-pointer hover:shadow-lg transition-shadow">
|
||||
<BaseButton
|
||||
href="/time_off_requests/time_off_requests-new?leave_type=unplanned_pto"
|
||||
icon={mdiAlertDecagram}
|
||||
label="Take Unplanned PTO"
|
||||
color="warning"
|
||||
className="w-full mb-2"
|
||||
iconSize={24}
|
||||
/>
|
||||
<p className="text-gray-500 text-sm text-center">Log unplanned absence.</p>
|
||||
</CardBox>
|
||||
</div>
|
||||
|
||||
{/* Action Items (Approvals) - Full Width */}
|
||||
<CardBox className="mb-6" hasTable>
|
||||
<div className="p-4 border-b dark:border-dark-700 flex justify-between items-center">
|
||||
<h3 className="font-bold">Action Items (Pending Approvals)</h3>
|
||||
<Link href="/approval_tasks/approval_tasks-list" className="text-sm text-blue-500 hover:underline">
|
||||
<h3 className="font-bold">Action Items (Pending Approvals)</h3>
|
||||
<Link href="/approval_tasks/approval_tasks-list" className="text-sm text-blue-500 hover:underline">
|
||||
View All
|
||||
</Link>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead>
|
||||
<tr className="border-b dark:border-dark-700">
|
||||
<tr className="border-b dark:border-dark-700">
|
||||
<th className="p-4">Requester</th>
|
||||
<th className="p-4">Type</th>
|
||||
<th className="p-4">Dates</th>
|
||||
<th className="p-4">Actions</th>
|
||||
</tr>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{approvals.length > 0 ? (
|
||||
{approvals.length > 0 ? (
|
||||
approvals.map((task) => (
|
||||
<tr key={task.id} className="border-b dark:border-dark-700">
|
||||
<tr key={task.id} className="border-b dark:border-dark-700">
|
||||
<td className="p-4">{task.time_off_request?.requester?.firstName} {task.time_off_request?.requester?.lastName}</td>
|
||||
<td className="p-4 capitalize">{task.task_type?.replace(/_/g, ' ')}</td>
|
||||
<td className="p-4">
|
||||
{moment(task.time_off_request?.starts_at).format('MMM D')} - {moment(task.time_off_request?.ends_at).format('MMM D')}
|
||||
{moment(task.time_off_request?.starts_at).format('MMM D')} - {moment(task.time_off_request?.ends_at).format('MMM D')}
|
||||
</td>
|
||||
<td className="p-4 whitespace-nowrap flex space-x-2">
|
||||
<BaseButton
|
||||
<BaseButton
|
||||
color="info"
|
||||
label="Review"
|
||||
small
|
||||
href={`/approval_tasks/approval_tasks-edit?id=${task.id}`}
|
||||
/>
|
||||
<BaseButton
|
||||
/>
|
||||
<BaseButton
|
||||
color="success"
|
||||
label="Approve"
|
||||
small
|
||||
onClick={() => handleApprove(task.id)}
|
||||
/>
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="p-4 text-center text-gray-500">No pending approvals</td>
|
||||
</tr>
|
||||
)}
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="p-4 text-center text-gray-500">No pending approvals</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
{/* Staff Off This Week - Full Width */}
|
||||
<StaffOffList />
|
||||
|
||||
</SectionMain>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1,12 +1,229 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import * as icon from '@mdi/js';
|
||||
import Head from 'next/head'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import axios from 'axios';
|
||||
import type { ReactElement } from 'react'
|
||||
import LayoutAuthenticated from '../layouts/Authenticated'
|
||||
import SectionMain from '../components/SectionMain'
|
||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
||||
import CardBox from '../components/CardBox'
|
||||
import BaseButton from '../components/BaseButton'
|
||||
import { getPageTitle } from '../config'
|
||||
import { useAppSelector } from '../stores/hooks'
|
||||
import PTOStats from '../components/PTOStats'
|
||||
import moment from 'moment'
|
||||
import Link from 'next/link'
|
||||
import StaffOffList from '../components/StaffOffList'
|
||||
import Search from '../components/Search'
|
||||
import { mdiPlusBox, mdiHospitalBox, mdiAlertDecagram } from '@mdi/js'
|
||||
|
||||
export default function Starter() {
|
||||
const router = useRouter();
|
||||
const Dashboard = () => {
|
||||
const { currentUser } = useAppSelector((state) => state.auth)
|
||||
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear())
|
||||
const [summary, setSummary] = useState(null)
|
||||
const [approvals, setApprovals] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [greeting, setGreeting] = useState('Hello')
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// Fetch PTO Summary for selected year
|
||||
const summaryRes = await axios.get(`/yearly_leave_summaries`, {
|
||||
params: {
|
||||
filter: JSON.stringify({
|
||||
userId: currentUser?.id,
|
||||
calendar_year: selectedYear
|
||||
})
|
||||
}
|
||||
})
|
||||
setSummary(summaryRes.data.rows[0] || null)
|
||||
|
||||
// Fetch Pending Approvals if manager/admin
|
||||
const approvalsRes = await axios.get(`/approval_tasks`, {
|
||||
params: {
|
||||
filter: JSON.stringify({
|
||||
state: 'open'
|
||||
})
|
||||
}
|
||||
})
|
||||
setApprovals(approvalsRes.data.rows)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard data:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
router.replace('/login');
|
||||
}, [router]);
|
||||
if (currentUser) {
|
||||
fetchDashboardData()
|
||||
}
|
||||
}, [currentUser, selectedYear])
|
||||
|
||||
return null;
|
||||
}
|
||||
useEffect(() => {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 12) setGreeting('Good morning')
|
||||
else if (hour < 18) setGreeting('Good afternoon')
|
||||
else setGreeting('Good evening')
|
||||
}, [])
|
||||
|
||||
const handleApprove = async (taskId) => {
|
||||
try {
|
||||
await axios.put(`/approval_tasks/${taskId}/approve`);
|
||||
// Refresh data
|
||||
fetchDashboardData();
|
||||
} catch (error) {
|
||||
console.error('Error approving task:', error);
|
||||
alert('Failed to approve task');
|
||||
}
|
||||
};
|
||||
|
||||
const years = [selectedYear - 1, selectedYear, selectedYear + 1, selectedYear + 2]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Home')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={icon.mdiHome}
|
||||
title={`${greeting}, ${currentUser?.firstName || 'User'}`}
|
||||
main
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-6">
|
||||
<Search />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-500">Year:</span>
|
||||
<select
|
||||
value={selectedYear}
|
||||
onChange={(e) => setSelectedYear(parseInt(e.target.value))}
|
||||
className="pl-2 pr-10 py-1 border rounded dark:bg-dark-800 dark:border-dark-700 dark:text-white"
|
||||
>
|
||||
{years.map(y => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
{/* PTO Summary Stats */}
|
||||
<PTOStats
|
||||
summary={summary || {
|
||||
pto_pending_days: 0,
|
||||
pto_scheduled_days: 0,
|
||||
pto_available_days: 0,
|
||||
medical_taken_days: 0
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<CardBox className="flex flex-col items-center justify-center p-6 cursor-pointer hover:shadow-lg transition-shadow" >
|
||||
<BaseButton
|
||||
href="/time_off_requests/time_off_requests-new"
|
||||
icon={mdiPlusBox}
|
||||
label="Request Time Off"
|
||||
color="info"
|
||||
className="w-full mb-2"
|
||||
iconSize={24}
|
||||
/>
|
||||
<p className="text-gray-500 text-sm text-center">Submit a standard PTO request.</p>
|
||||
</CardBox>
|
||||
|
||||
<CardBox className="flex flex-col items-center justify-center p-6 cursor-pointer hover:shadow-lg transition-shadow">
|
||||
<BaseButton
|
||||
href="/time_off_requests/time_off_requests-new?leave_type=medical_leave"
|
||||
icon={mdiHospitalBox}
|
||||
label="Take Medical Day"
|
||||
color="danger"
|
||||
className="w-full mb-2"
|
||||
iconSize={24}
|
||||
/>
|
||||
<p className="text-gray-500 text-sm text-center">Report a sick day or medical leave.</p>
|
||||
</CardBox>
|
||||
|
||||
<CardBox className="flex flex-col items-center justify-center p-6 cursor-pointer hover:shadow-lg transition-shadow">
|
||||
<BaseButton
|
||||
href="/time_off_requests/time_off_requests-new?leave_type=unplanned_pto"
|
||||
icon={mdiAlertDecagram}
|
||||
label="Take Unplanned PTO"
|
||||
color="warning"
|
||||
className="w-full mb-2"
|
||||
iconSize={24}
|
||||
/>
|
||||
<p className="text-gray-500 text-sm text-center">Log unplanned absence.</p>
|
||||
</CardBox>
|
||||
</div>
|
||||
|
||||
{/* Action Items (Approvals) - Full Width */}
|
||||
<CardBox className="mb-6" hasTable>
|
||||
<div className="p-4 border-b dark:border-dark-700 flex justify-between items-center">
|
||||
<h3 className="font-bold">Action Items (Pending Approvals)</h3>
|
||||
<Link href="/approval_tasks/approval_tasks-list" className="text-sm text-blue-500 hover:underline">
|
||||
View All
|
||||
</Link>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead>
|
||||
<tr className="border-b dark:border-dark-700">
|
||||
<th className="p-4">Requester</th>
|
||||
<th className="p-4">Type</th>
|
||||
<th className="p-4">Dates</th>
|
||||
<th className="p-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{approvals.length > 0 ? (
|
||||
approvals.map((task) => (
|
||||
<tr key={task.id} className="border-b dark:border-dark-700">
|
||||
<td className="p-4">{task.time_off_request?.requester?.firstName} {task.time_off_request?.requester?.lastName}</td>
|
||||
<td className="p-4 capitalize">{task.task_type?.replace(/_/g, ' ')}</td>
|
||||
<td className="p-4">
|
||||
{moment(task.time_off_request?.starts_at).format('MMM D')} - {moment(task.time_off_request?.ends_at).format('MMM D')}
|
||||
</td>
|
||||
<td className="p-4 whitespace-nowrap flex space-x-2">
|
||||
<BaseButton
|
||||
color="info"
|
||||
label="Review"
|
||||
small
|
||||
href={`/approval_tasks/approval_tasks-edit?id=${task.id}`}
|
||||
/>
|
||||
<BaseButton
|
||||
color="success"
|
||||
label="Approve"
|
||||
small
|
||||
onClick={() => handleApprove(task.id)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="p-4 text-center text-gray-500">No pending approvals</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
{/* Staff Off This Week - Full Width */}
|
||||
<StaffOffList />
|
||||
|
||||
</SectionMain>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Dashboard.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
@ -93,12 +93,13 @@ const Time_off_requestsNew = () => {
|
||||
if (currentUser) {
|
||||
setFormInitialValues((prev) => ({
|
||||
...prev,
|
||||
requester: currentUser.id,
|
||||
requester: currentUser,
|
||||
submitted_at: moment().format('YYYY-MM-DDTHH:mm'),
|
||||
date_requested: moment().format('YYYY-MM-DD')
|
||||
date_requested: moment().format('YYYY-MM-DD'),
|
||||
leave_type: (router.query.leave_type as string) || prev.leave_type
|
||||
}));
|
||||
}
|
||||
}, [currentUser]);
|
||||
}, [currentUser, router.query]);
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
// Ensure hidden fields are set correctly if form didn't touch them
|
||||
@ -131,6 +132,8 @@ const Time_off_requestsNew = () => {
|
||||
payload.submitted_at = moment().format('YYYY-MM-DDTHH:mm');
|
||||
if (currentUser && !payload.requester) {
|
||||
payload.requester = currentUser.id;
|
||||
} else if (typeof payload.requester === 'object' && payload.requester?.id) {
|
||||
payload.requester = payload.requester.id;
|
||||
}
|
||||
|
||||
await dispatch(create(payload))
|
||||
@ -160,7 +163,14 @@ const Time_off_requestsNew = () => {
|
||||
{/* Requester - Only visible to Admin */}
|
||||
{isAdmin && (
|
||||
<FormField label="Requester" labelFor="requester">
|
||||
<Field name="requester" id="requester" component={SelectField} options={[]} itemRef={'users'}></Field>
|
||||
<Field
|
||||
name="requester"
|
||||
id="requester"
|
||||
component={SelectField}
|
||||
options={currentUser}
|
||||
itemRef={'users'}
|
||||
showField="firstName"
|
||||
></Field>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
@ -228,4 +238,4 @@ Time_off_requestsNew.getLayout = function getLayout(page: ReactElement) {
|
||||
)
|
||||
}
|
||||
|
||||
export default Time_off_requestsNew
|
||||
export default Time_off_requestsNew
|
||||
Loading…
x
Reference in New Issue
Block a user