diff --git a/backend/src/routes/time_off_requests.js b/backend/src/routes/time_off_requests.js index 2e2e85d..37ae26c 100644 --- a/backend/src/routes/time_off_requests.js +++ b/backend/src/routes/time_off_requests.js @@ -1,4 +1,3 @@ - const express = require('express'); const Time_off_requestsService = require('../services/time_off_requests'); @@ -303,19 +302,33 @@ router.get('/', wrapAsync(async (req, res) => { req.query, { currentUser } ); if (filetype && filetype === 'csv') { - const fields = ['id','reason','manager_note','external_reference', - - 'hours','days', - 'starts_at','ends_at','submitted_at','decided_at', - ]; + const fields = [ + { label: 'ID', value: 'id' }, + { label: 'Requester', value: (row) => row.requester ? `${row.requester.firstName} ${row.requester.lastName} (${row.requester.email})` : '' }, + { label: 'Approver', value: (row) => row.approver ? `${row.approver.firstName} ${row.approver.lastName} (${row.approver.email})` : '' }, + { label: 'Type', value: 'leave_type' }, + { label: 'Request Kind', value: 'request_kind' }, + { label: 'Start', value: 'starts_at' }, + { label: 'Finish', value: 'ends_at' }, + { label: 'Hours', value: 'hours' }, + { label: 'Days', value: 'days' }, + { label: 'Status', value: 'status' }, + { label: 'Requires Approval', value: 'requires_approval' }, + { label: 'Submitted At', value: 'submitted_at' }, + { label: 'Decided At', value: 'decided_at' }, + { label: 'Reason', value: 'reason' }, + { label: 'Manager Note', value: 'manager_note' }, + { label: 'External Reference', value: 'external_reference' } + ]; const opts = { fields }; try { const csv = parse(payload.rows, opts); - res.status(200).attachment(csv); + res.status(200).attachment('time_off_requests.csv'); res.send(csv) } catch (err) { console.error(err); + res.status(500).send('Error generating CSV'); } } else { res.status(200).send(payload); @@ -441,4 +454,4 @@ router.get('/:id', wrapAsync(async (req, res) => { router.use('/', require('../helpers').commonErrorHandler); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/frontend/src/components/Time_off_requests/TableTime_off_requests.tsx b/frontend/src/components/Time_off_requests/TableTime_off_requests.tsx index 6b78eae..c2c0a8d 100644 --- a/frontend/src/components/Time_off_requests/TableTime_off_requests.tsx +++ b/frontend/src/components/Time_off_requests/TableTime_off_requests.tsx @@ -12,6 +12,9 @@ import { DataGrid, GridColDef, } from '@mui/x-data-grid'; +import { Drawer, Box, Typography, IconButton } from '@mui/material'; +import { mdiClose } from '@mdi/js'; +import BaseIcon from '../BaseIcon'; import {loadColumns} from "./configureTime_off_requestsCols"; import _ from 'lodash'; import dataFormatter from '../../helpers/dataFormatter' @@ -20,12 +23,20 @@ import moment from 'moment'; import KanbanBoard from '../KanbanBoard/KanbanBoard'; -import axios from 'axios'; const perPage = 10 -const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, showGrid }) => { +const getUserLabel = (user: any) => { + if (!user) return ''; + if (user.label) return user.label; + if (user.firstName || user.lastName) { + return `${user.firstName || ''} ${user.lastName || ''}`.trim(); + } + return user.id || ''; +} + +const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, showGrid, isFilterDrawerOpen, setIsFilterDrawerOpen }) => { const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const dispatch = useAppDispatch(); @@ -131,11 +142,14 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh const [isModalInfoActive, setIsModalInfoActive] = useState(false) const [isModalTrashActive, setIsModalTrashActive] = useState(false) const [isModalCancelActive, setIsModalCancelActive] = useState(false) + const [isModalViewActive, setIsModalViewActive] = useState(false) + const [selectedItem, setSelectedItem] = useState(null) const handleModalAction = () => { setIsModalInfoActive(false) setIsModalTrashActive(false) setIsModalCancelActive(false) + setIsModalViewActive(false) } @@ -202,6 +216,11 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh } } + const handleViewAction = (row: any) => { + setSelectedItem(row); + setIsModalViewActive(true); + } + const generateFilterRequests = useMemo(() => { let request = '&'; filterItems.forEach((item) => { @@ -245,10 +264,10 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh }; const handleSubmit = () => { - loadData(0, generateFilterRequests); + loadData(currentPage, generateFilterRequests); setKanbanFilters(generateFilterRequests); - + setIsFilterDrawerOpen(false); }; const handleChange = (id) => (e) => { @@ -270,7 +289,7 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh loadData(0, ''); setKanbanFilters(''); - + setIsFilterDrawerOpen(false); }; const onPageChange = (page: number) => { @@ -286,7 +305,8 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh handleDeleteModalAction, `time_off_requests`, currentUser, - handleCancelModalAction + handleCancelModalAction, + handleViewAction ).then((newCols) => setColumns(newCols)); }, [currentUser]); @@ -385,175 +405,195 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh ) + const addFilter = () => { + const newItem = { + id: _.uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0]?.title || ''; + setFilterItems([...filterItems, newItem]); + }; + return ( <> - {filterItems && Array.isArray( filterItems ) && filterItems.length ? - + setIsFilterDrawerOpen(false)} + ModalProps={{ + keepMounted: true, // Better open performance on mobile + }} + > + +
+ Filters + setIsFilterDrawerOpen(false)}> + + +
null} >
<> {filterItems && filterItems.map((filterItem) => { + const selectedFilter = filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + ); + return ( -
-
-
Filter
- - {filters.map((selectOption) => ( - - ))} - -
- {filters.find((filter) => - filter.title === filterItem?.fields?.selectedField - )?.type === 'enum' ? ( -
-
- Value -
- - - {filters.find((filter) => - filter.title === filterItem?.fields?.selectedField - )?.options?.map((option) => ( - - ))} - -
- ) : filters.find((filter) => - filter.title === filterItem?.fields?.selectedField - )?.number ? ( -
-
-
From
- -
-
-
To
- -
-
- ) : filters.find( - (filter) => - filter.title === - filterItem?.fields?.selectedField - )?.date ? ( -
-
-
- From -
- -
-
-
To
- -
-
- ) : ( -
-
Contains
- -
- )} -
-
Action
+ +
+ Condition { deleteFilter(filterItem.id) }} />
-
+
+
+
Field
+ + {filters.map((selectOption) => ( + + ))} + +
+ {selectedFilter?.type === 'enum' ? ( +
+
Value
+ + + {selectedFilter?.options?.map((option) => ( + + ))} + +
+ ) : selectedFilter?.number ? ( +
+
+
From
+ +
+
+
To
+ +
+
+ ) : selectedFilter?.date ? ( +
+
+
From
+ +
+
+
To
+ +
+
+ ) : ( +
+
Contains
+ +
+ )} +
+ ) })} -
+
+
- : null - } + + + Are you sure you want to cancel this time off request?

+ + {selectedItem && ( +
+

Requester: {getUserLabel(selectedItem.requester)}

+

Approver: {getUserLabel(selectedItem.approver)}

+

Type: {selectedItem.leave_type}

+

Kind: {selectedItem.request_kind}

+

Start: {moment(selectedItem.starts_at).format('YYYY-MM-DD HH:mm')}

+

Finish: {moment(selectedItem.ends_at).format('YYYY-MM-DD HH:mm')}

+

Hours: {selectedItem.hours}

+

Days: {selectedItem.days}

+

Status: {selectedItem.status}

+

Requires Approval: {selectedItem.requires_approval ? 'Yes' : 'No'}

+

Submitted At: {selectedItem.submitted_at ? moment(selectedItem.submitted_at).format('YYYY-MM-DD HH:mm') : 'N/A'}

+

Decided At: {selectedItem.decided_at ? moment(selectedItem.decided_at).format('YYYY-MM-DD HH:mm') : 'N/A'}

+

Reason: {selectedItem.reason}

+

Manager Note: {selectedItem.manager_note}

+

External Reference: {selectedItem.external_reference}

+
+ )} +
+ {!showGrid && kanbanColumns && ( diff --git a/frontend/src/components/Time_off_requests/configureTime_off_requestsCols.tsx b/frontend/src/components/Time_off_requests/configureTime_off_requestsCols.tsx index f139cf6..224eefb 100644 --- a/frontend/src/components/Time_off_requests/configureTime_off_requestsCols.tsx +++ b/frontend/src/components/Time_off_requests/configureTime_off_requestsCols.tsx @@ -1,29 +1,31 @@ import React from 'react'; -import BaseIcon from '../BaseIcon'; -import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; import axios from 'axios'; import { - GridActionsCellItem, - GridRowParams, GridValueGetterParams, } from '@mui/x-data-grid'; -import ImageField from '../ImageField'; -import {saveFile} from "../../helpers/fileSaver"; -import dataFormatter from '../../helpers/dataFormatter' -import DataGridMultiSelect from "../DataGridMultiSelect"; import ListActionsPopover from '../ListActionsPopover'; import {hasPermission} from "../../helpers/userPermissions"; type Params = (id: string) => void; +const getUserLabel = (user: any) => { + if (!user) return ''; + if (user.label) return user.label; + if (user.firstName || user.lastName) { + return `${user.firstName || ''} ${user.lastName || ''}`.trim(); + } + return user.id || ''; +} + export const loadColumns = async ( onDelete: Params, entityName: string, user, - onCancel?: Params + onCancel?: Params, + onView?: (row: any) => void ) => { async function callOptionsApi(entityName: string) { @@ -39,18 +41,26 @@ export const loadColumns = async ( } const hasUpdatePermission = hasPermission(user, 'UPDATE_TIME_OFF_REQUESTS') + const isAdmin = user?.app_role?.name === 'Administrator'; - return [ + const columns: any[] = [ { field: 'requester', headerName: 'Requester', flex: 1, - minWidth: 120, + minWidth: 150, filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + renderCell: (params: GridValueGetterParams) => ( + onView && onView(params.row)} + > + {getUserLabel(params?.row?.requester)} + + ), editable: hasUpdatePermission, @@ -68,11 +78,15 @@ export const loadColumns = async ( field: 'approver', headerName: 'Approver', flex: 1, - minWidth: 120, + minWidth: 150, filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + renderCell: (params: GridValueGetterParams) => ( + + {getUserLabel(params?.row?.approver)} + + ), editable: hasUpdatePermission, @@ -88,7 +102,7 @@ export const loadColumns = async ( { field: 'leave_type', - headerName: 'LeaveType', + headerName: 'Type', flex: 1, minWidth: 120, filterable: false, @@ -109,7 +123,7 @@ export const loadColumns = async ( filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + hide: !isAdmin, editable: hasUpdatePermission, @@ -118,7 +132,7 @@ export const loadColumns = async ( { field: 'starts_at', - headerName: 'StartsAt', + headerName: 'Start', flex: 1, minWidth: 120, filterable: false, @@ -136,7 +150,7 @@ export const loadColumns = async ( { field: 'ends_at', - headerName: 'EndsAt', + headerName: 'Finish', flex: 1, minWidth: 120, filterable: false, @@ -160,7 +174,7 @@ export const loadColumns = async ( filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + hide: true, editable: hasUpdatePermission, @@ -207,7 +221,7 @@ export const loadColumns = async ( filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + hide: true, // Hidden by default editable: hasUpdatePermission, @@ -223,7 +237,7 @@ export const loadColumns = async ( filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + hide: true, // Hidden by default editable: hasUpdatePermission, @@ -241,7 +255,7 @@ export const loadColumns = async ( filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + hide: true, // Hidden by default editable: hasUpdatePermission, @@ -259,7 +273,7 @@ export const loadColumns = async ( filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + hide: true, // Hidden by default editable: hasUpdatePermission, @@ -274,7 +288,7 @@ export const loadColumns = async ( filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + hide: true, // Hidden by default editable: hasUpdatePermission, @@ -289,22 +303,9 @@ export const loadColumns = async ( filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + hide: true, editable: false, sortable: false, - renderCell: (params: GridValueGetterParams) => ( - <> - {dataFormatter.filesFormatter(params.row.attachments).map(link => ( - - ))} - - ), - }, { @@ -315,7 +316,7 @@ export const loadColumns = async ( filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + hide: true, editable: hasUpdatePermission, @@ -328,7 +329,8 @@ export const loadColumns = async ( minWidth: 30, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - getActions: (params: GridRowParams) => { + hide: true, + getActions: (params: any) => { return [
@@ -348,4 +350,14 @@ export const loadColumns = async ( }, }, ]; + + // Filter columns for non-admins if they are strictly admin-only + return columns.filter(col => { + if (!isAdmin) { + if (['requires_approval', 'submitted_at', 'decided_at', 'reason', 'manager_note'].includes(col.field)) { + return false; + } + } + return true; + }); }; \ No newline at end of file diff --git a/frontend/src/pages/time_off_requests/time_off_requests-list.tsx b/frontend/src/pages/time_off_requests/time_off_requests-list.tsx index ff52b44..920b235 100644 --- a/frontend/src/pages/time_off_requests/time_off_requests-list.tsx +++ b/frontend/src/pages/time_off_requests/time_off_requests-list.tsx @@ -1,6 +1,5 @@ import { mdiChartTimelineVariant } from '@mdi/js' import Head from 'next/head' -import { uniqueId } from 'lodash'; import React, { ReactElement, useState } from 'react' import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' @@ -27,6 +26,7 @@ const Time_off_requestsTablesPage = () => { const [csvFile, setCsvFile] = useState(null); const [isModalActive, setIsModalActive] = useState(false); const [showTableView, setShowTableView] = useState(true); + const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false); const { currentUser } = useAppSelector((state) => state.auth); @@ -35,10 +35,10 @@ const Time_off_requestsTablesPage = () => { const dispatch = useAppDispatch(); - const [filters] = useState([{label: 'Reason', title: 'reason'},{label: 'ManagerNote', title: 'manager_note'},{label: 'ExternalReference', title: 'external_reference'}, + const [filters] = useState([{label: 'Reason', title: 'reason'},{label: 'Manager Note', title: 'manager_note'},{label: 'External Reference', title: 'external_reference'}, {label: 'Hours', title: 'hours', number: 'true'},{label: 'Days', title: 'days', number: 'true'}, - {label: 'StartsAt', title: 'starts_at', date: 'true'},{label: 'EndsAt', title: 'ends_at', date: 'true'},{label: 'SubmittedAt', title: 'submitted_at', date: 'true'},{label: 'DecidedAt', title: 'decided_at', date: 'true'}, + {label: 'Start', title: 'starts_at', date: 'true'},{label: 'Finish', title: 'ends_at', date: 'true'},{label: 'Submitted At', title: 'submitted_at', date: 'true'},{label: 'Decided At', title: 'decided_at', date: 'true'}, {label: 'Requester', title: 'requester'}, @@ -50,7 +50,7 @@ const Time_off_requestsTablesPage = () => { - {label: 'LeaveType', title: 'leave_type', type: 'enum', options: ['regular_pto','unplanned_pto','medical_leave','bereavement','time_in_lieu']},{label: 'RequestKind', title: 'request_kind', type: 'enum', options: ['take_time_off','add_time_credit','manual_adjustment']},{label: 'Status', title: 'status', type: 'enum', options: ['draft','pending_approval','approved','rejected','cancelled']}, + {label: 'Type', title: 'leave_type', type: 'enum', options: ['regular_pto','unplanned_pto','medical_leave','bereavement','time_in_lieu']},{label: 'Request Kind', title: 'request_kind', type: 'enum', options: ['take_time_off','add_time_credit','manual_adjustment']},{label: 'Status', title: 'status', type: 'enum', options: ['draft','pending_approval','approved','rejected','cancelled']}, ]); const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_TIME_OFF_REQUESTS'); @@ -62,27 +62,13 @@ const Time_off_requestsTablesPage = () => { }, undefined, { shallow: true }); }; - const addFilter = () => { - const newItem = { - id: uniqueId(), - fields: { - filterValue: '', - filterValueFrom: '', - filterValueTo: '', - selectedField: '', - }, - }; - newItem.fields.selectedField = filters[0].title; - setFilterItems([...filterItems, newItem]); - }; - const getTime_off_requestsCSV = async () => { const response = await axios({url: '/time_off_requests?filetype=csv', method: 'GET',responseType: 'blob'}); const type = response.headers['content-type'] const blob = new Blob([response.data], { type: type }) const link = document.createElement('a') link.href = window.URL.createObjectURL(blob) - link.download = 'time_off_requestsCSV.csv' + link.download = 'time_off_requests_export.csv' link.click() }; @@ -102,10 +88,10 @@ const Time_off_requestsTablesPage = () => { return ( <> - {getPageTitle('Time_off_requests')} + {getPageTitle('Time Off Requests')} - + {''} @@ -116,7 +102,7 @@ const Time_off_requestsTablesPage = () => { className={'mr-3'} color='info' label='Filter' - onClick={addFilter} + onClick={() => setIsFilterDrawerOpen(true)} /> { label='Pending' onClick={showPending} /> - + {hasCreatePermission && ( setIsModalActive(true)} @@ -153,6 +140,8 @@ const Time_off_requestsTablesPage = () => { setFilterItems={setFilterItems} filters={filters} showGrid={showTableView} + isFilterDrawerOpen={isFilterDrawerOpen} + setIsFilterDrawerOpen={setIsFilterDrawerOpen} />