From a7b1502197796ba9623efb95d50def0adcd38b86 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 17 Feb 2026 03:47:02 +0000 Subject: [PATCH] Autosave: 20260217-034702 --- frontend/src/components/NavBar.tsx | 28 ++- frontend/src/components/StaffOffList.tsx | 132 ++++++++++ frontend/src/layouts/Authenticated.tsx | 10 +- frontend/src/menuNavBar.ts | 17 +- frontend/src/pages/dashboard.tsx | 135 +++++++--- frontend/src/pages/index.tsx | 233 +++++++++++++++++- .../time_off_requests-new.tsx | 20 +- 7 files changed, 502 insertions(+), 73 deletions(-) create mode 100644 frontend/src/components/StaffOffList.tsx diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx index c270ae0..99fe27b 100644 --- a/frontend/src/components/NavBar.tsx +++ b/frontend/src/components/NavBar.tsx @@ -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`} >
-
{children}
-
- - - + + {/* Main Menu (Desktop) - Contains All Items including Profile - Centered */} +
+
+ + {/* Mobile Toggle */} +
+ + + +
+ + {/* Mobile Menu Dropdown */}
- +
+ +
) -} +} \ No newline at end of file diff --git a/frontend/src/components/StaffOffList.tsx b/frontend/src/components/StaffOffList.tsx new file mode 100644 index 0000000..201a081 --- /dev/null +++ b/frontend/src/components/StaffOffList.tsx @@ -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([]); + 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 ( + +
+

Staff off this week

+
+ + {weekStart.format('MMM D')} - {weekStart.clone().endOf('isoWeek').format('MMM D')} + +
+
+ + {loading ? ( +
Loading...
+ ) : requests.length === 0 ? ( +
No staff off this week.
+ ) : ( +
+ {requests.map((r) => ( +
+
+
+ + {r.requester?.firstName?.[0]}{r.requester?.lastName?.[0]} + +
+
+
{r.requester?.firstName} {r.requester?.lastName}
+
+ + {r.leave_type.replace('_', ' ')} +
+
+
+
+ {moment(r.starts_at).format('ddd D')} - {moment(r.ends_at).format('ddd D')} +
+
+ ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 45a433b..a2966d1 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -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({ - - - - + />
{children}
@@ -115,4 +109,4 @@ export default function LayoutAuthenticated({
) -} +} \ No newline at end of file diff --git a/frontend/src/menuNavBar.ts b/frontend/src/menuNavBar.ts index 3b7b1ca..87c8dfc 100644 --- a/frontend/src/menuNavBar.ts +++ b/frontend/src/menuNavBar.ts @@ -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 = [ diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 4a8eac1..62b58fd 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -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 = () => { {getPageTitle('Home')} - -
- Year: - + +
+
+ +
+
+ Year: + +
@@ -102,60 +122,101 @@ const Dashboard = () => { }} /> -
- {/* Action Items (Approvals) */} - + {/* Action Buttons */} +
+ + +

Submit a standard PTO request.

+
+ + + +

Report a sick day or medical leave.

+
+ + + +

Log unplanned absence.

+
+
+ + {/* Action Items (Approvals) - Full Width */} +
-

Action Items (Pending Approvals)

- +

Action Items (Pending Approvals)

+ View All - +
- +
- + - + - {approvals.length > 0 ? ( + {approvals.length > 0 ? ( approvals.map((task) => ( - + - - )) - ) : ( - - - )} + )) + ) : ( + + + + )} -
Requester Type Dates Actions
{task.time_off_request?.requester?.firstName} {task.time_off_request?.requester?.lastName} {task.task_type?.replace(/_/g, ' ')} - {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')} - - + handleApprove(task.id)} - /> + />
No pending approvals
No pending approvals
+
-
-
+ + + {/* Staff Off This Week - Full Width */} + + ) diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 49dd63f..62b58fd 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -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; -} \ No newline at end of file + 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 ( + <> + + {getPageTitle('Home')} + + + +
+
+ +
+
+ Year: + +
+
+
+ + {/* PTO Summary Stats */} + + + {/* Action Buttons */} +
+ + +

Submit a standard PTO request.

+
+ + + +

Report a sick day or medical leave.

+
+ + + +

Log unplanned absence.

+
+
+ + {/* Action Items (Approvals) - Full Width */} + +
+

Action Items (Pending Approvals)

+ + View All + +
+
+ + + + + + + + + + + {approvals.length > 0 ? ( + approvals.map((task) => ( + + + + + + + )) + ) : ( + + + + )} + +
RequesterTypeDatesActions
{task.time_off_request?.requester?.firstName} {task.time_off_request?.requester?.lastName}{task.task_type?.replace(/_/g, ' ')} + {moment(task.time_off_request?.starts_at).format('MMM D')} - {moment(task.time_off_request?.ends_at).format('MMM D')} + + + handleApprove(task.id)} + /> +
No pending approvals
+
+
+ + {/* Staff Off This Week - Full Width */} + + +
+ + ) +} + +Dashboard.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default Dashboard \ No newline at end of file diff --git a/frontend/src/pages/time_off_requests/time_off_requests-new.tsx b/frontend/src/pages/time_off_requests/time_off_requests-new.tsx index 59802be..2e103f3 100644 --- a/frontend/src/pages/time_off_requests/time_off_requests-new.tsx +++ b/frontend/src/pages/time_off_requests/time_off_requests-new.tsx @@ -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 && ( - + )} @@ -228,4 +238,4 @@ Time_off_requestsNew.getLayout = function getLayout(page: ReactElement) { ) } -export default Time_off_requestsNew +export default Time_off_requestsNew \ No newline at end of file