changes to time off requests

This commit is contained in:
Flatlogic Bot 2026-02-17 18:57:20 +00:00
parent c1f17908e1
commit 40b50cdd6c
4 changed files with 301 additions and 218 deletions

View File

@ -1,4 +1,3 @@
const express = require('express'); const express = require('express');
const Time_off_requestsService = require('../services/time_off_requests'); const Time_off_requestsService = require('../services/time_off_requests');
@ -303,19 +302,33 @@ router.get('/', wrapAsync(async (req, res) => {
req.query, { currentUser } req.query, { currentUser }
); );
if (filetype && filetype === 'csv') { if (filetype && filetype === 'csv') {
const fields = ['id','reason','manager_note','external_reference', const fields = [
{ label: 'ID', value: 'id' },
'hours','days', { label: 'Requester', value: (row) => row.requester ? `${row.requester.firstName} ${row.requester.lastName} (${row.requester.email})` : '' },
'starts_at','ends_at','submitted_at','decided_at', { 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 }; const opts = { fields };
try { try {
const csv = parse(payload.rows, opts); const csv = parse(payload.rows, opts);
res.status(200).attachment(csv); res.status(200).attachment('time_off_requests.csv');
res.send(csv) res.send(csv)
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.status(500).send('Error generating CSV');
} }
} else { } else {
res.status(200).send(payload); res.status(200).send(payload);

View File

@ -12,6 +12,9 @@ import {
DataGrid, DataGrid,
GridColDef, GridColDef,
} from '@mui/x-data-grid'; } 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 {loadColumns} from "./configureTime_off_requestsCols";
import _ from 'lodash'; import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter' import dataFormatter from '../../helpers/dataFormatter'
@ -20,12 +23,20 @@ import moment from 'moment';
import KanbanBoard from '../KanbanBoard/KanbanBoard'; import KanbanBoard from '../KanbanBoard/KanbanBoard';
import axios from 'axios';
const perPage = 10 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 notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -131,11 +142,14 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh
const [isModalInfoActive, setIsModalInfoActive] = useState(false) const [isModalInfoActive, setIsModalInfoActive] = useState(false)
const [isModalTrashActive, setIsModalTrashActive] = useState(false) const [isModalTrashActive, setIsModalTrashActive] = useState(false)
const [isModalCancelActive, setIsModalCancelActive] = useState(false) const [isModalCancelActive, setIsModalCancelActive] = useState(false)
const [isModalViewActive, setIsModalViewActive] = useState(false)
const [selectedItem, setSelectedItem] = useState<any>(null)
const handleModalAction = () => { const handleModalAction = () => {
setIsModalInfoActive(false) setIsModalInfoActive(false)
setIsModalTrashActive(false) setIsModalTrashActive(false)
setIsModalCancelActive(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(() => { const generateFilterRequests = useMemo(() => {
let request = '&'; let request = '&';
filterItems.forEach((item) => { filterItems.forEach((item) => {
@ -245,10 +264,10 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh
}; };
const handleSubmit = () => { const handleSubmit = () => {
loadData(0, generateFilterRequests); loadData(currentPage, generateFilterRequests);
setKanbanFilters(generateFilterRequests); setKanbanFilters(generateFilterRequests);
setIsFilterDrawerOpen(false);
}; };
const handleChange = (id) => (e) => { const handleChange = (id) => (e) => {
@ -270,7 +289,7 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh
loadData(0, ''); loadData(0, '');
setKanbanFilters(''); setKanbanFilters('');
setIsFilterDrawerOpen(false);
}; };
const onPageChange = (page: number) => { const onPageChange = (page: number) => {
@ -286,7 +305,8 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh
handleDeleteModalAction, handleDeleteModalAction,
`time_off_requests`, `time_off_requests`,
currentUser, currentUser,
handleCancelModalAction handleCancelModalAction,
handleViewAction
).then((newCols) => setColumns(newCols)); ).then((newCols) => setColumns(newCols));
}, [currentUser]); }, [currentUser]);
@ -385,175 +405,195 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh
</div> </div>
) )
const addFilter = () => {
const newItem = {
id: _.uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0]?.title || '';
setFilterItems([...filterItems, newItem]);
};
return ( return (
<> <>
{filterItems && Array.isArray( filterItems ) && filterItems.length ? <Drawer
<CardBox> anchor="right"
open={isFilterDrawerOpen}
onClose={() => setIsFilterDrawerOpen(false)}
ModalProps={{
keepMounted: true, // Better open performance on mobile
}}
>
<Box sx={{ width: 350, p: 4 }}>
<div className="flex justify-between items-center mb-6">
<Typography variant="h6">Filters</Typography>
<IconButton onClick={() => setIsFilterDrawerOpen(false)}>
<BaseIcon path={mdiClose} />
</IconButton>
</div>
<Formik <Formik
initialValues={{ initialValues={{}}
checkboxes: ['lorem'],
switches: ['lorem'],
radio: 'lorem',
}}
onSubmit={() => null} onSubmit={() => null}
> >
<Form> <Form>
<> <>
{filterItems && filterItems.map((filterItem) => { {filterItems && filterItems.map((filterItem) => {
const selectedFilter = filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
);
return ( return (
<div key={filterItem.id} className="flex mb-4"> <CardBox key={filterItem.id} className="mb-4" noPadding>
<div className="flex flex-col w-full mr-3"> <div className="p-4 border-b border-gray-100 dark:border-gray-700 flex justify-between items-center">
<div className=" text-gray-500 font-bold">Filter</div> <span className="font-bold text-gray-500 text-sm">Condition</span>
<Field
className={controlClasses}
name='selectedField'
id='selectedField'
component='select'
value={filterItem?.fields?.selectedField || ''}
onChange={handleChange(filterItem.id)}
>
{filters.map((selectOption) => (
<option
key={selectOption.title}
value={`${selectOption.title}`}
>
{selectOption.label}
</option>
))}
</Field>
</div>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
</div>
<Field
className={controlClasses}
name="filterValue"
id='filterValue'
component="select"
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</Field>
</div>
) : filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.number ? (
<div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
</div>
) : filters.find(
(filter) =>
filter.title ===
filterItem?.fields?.selectedField
)?.date ? (
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<BaseButton <BaseButton
className="my-2"
type='reset'
color='danger' color='danger'
label='Delete' label='Remove'
small
onClick={() => { onClick={() => {
deleteFilter(filterItem.id) deleteFilter(filterItem.id)
}} }}
/> />
</div> </div>
</div> <div className="p-4">
<div className="mb-2">
<div className="text-gray-500 text-xs mb-1 font-bold">Field</div>
<Field
className={controlClasses}
name='selectedField'
id='selectedField'
component='select'
value={filterItem?.fields?.selectedField || ''}
onChange={handleChange(filterItem.id)}
>
{filters.map((selectOption) => (
<option
key={selectOption.title}
value={`${selectOption.title}`}
>
{selectOption.label}
</option>
))}
</Field>
</div>
{selectedFilter?.type === 'enum' ? (
<div className="mb-2">
<div className="text-gray-500 text-xs mb-1 font-bold">Value</div>
<Field
className={controlClasses}
name="filterValue"
id='filterValue'
component="select"
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
{selectedFilter?.options?.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</Field>
</div>
) : selectedFilter?.number ? (
<div className="flex space-x-2">
<div className="w-1/2">
<div className="text-gray-500 text-xs mb-1 font-bold">From</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="w-1/2">
<div className="text-gray-500 text-xs mb-1 font-bold">To</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='To'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
</div>
) : selectedFilter?.date ? (
<div className="flex space-x-2">
<div className="w-1/2">
<div className="text-gray-500 text-xs mb-1 font-bold">From</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="w-1/2">
<div className="text-gray-500 text-xs mb-1 font-bold">To</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='To'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
</div>
) : (
<div className="mb-2">
<div className="text-gray-500 text-xs mb-1 font-bold">Contains</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
)}
</div>
</CardBox>
) )
})} })}
<div className="flex"> <div className="mt-6 flex flex-col space-y-3">
<BaseButton <BaseButton
className="my-2 mr-3" color="info"
color="success" label='Add Condition'
label='Apply' outline
onClick={addFilter}
/>
<BaseButton
color="success"
label='Apply Filters'
onClick={handleSubmit} onClick={handleSubmit}
/> />
<BaseButton <BaseButton
className="my-2" color='info'
color='info' label='Clear All'
label='Cancel' outline
onClick={handleReset} onClick={handleReset}
/> />
</div> </div>
</> </>
</Form> </Form>
</Formik> </Formik>
</CardBox> : null </Box>
} </Drawer>
<CardBoxModal <CardBoxModal
title="Please confirm" title="Please confirm"
buttonColor="info" buttonColor="info"
@ -576,6 +616,35 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh
<p>Are you sure you want to cancel this time off request?</p> <p>Are you sure you want to cancel this time off request?</p>
</CardBoxModal> </CardBoxModal>
<CardBoxModal
title="Request Details"
buttonColor="info"
buttonLabel="Close"
isActive={isModalViewActive}
onConfirm={handleModalAction}
onCancel={handleModalAction}
>
{selectedItem && (
<div className="space-y-2">
<p><strong>Requester:</strong> {getUserLabel(selectedItem.requester)}</p>
<p><strong>Approver:</strong> {getUserLabel(selectedItem.approver)}</p>
<p><strong>Type:</strong> {selectedItem.leave_type}</p>
<p><strong>Kind:</strong> {selectedItem.request_kind}</p>
<p><strong>Start:</strong> {moment(selectedItem.starts_at).format('YYYY-MM-DD HH:mm')}</p>
<p><strong>Finish:</strong> {moment(selectedItem.ends_at).format('YYYY-MM-DD HH:mm')}</p>
<p><strong>Hours:</strong> {selectedItem.hours}</p>
<p><strong>Days:</strong> {selectedItem.days}</p>
<p><strong>Status:</strong> {selectedItem.status}</p>
<p><strong>Requires Approval:</strong> {selectedItem.requires_approval ? 'Yes' : 'No'}</p>
<p><strong>Submitted At:</strong> {selectedItem.submitted_at ? moment(selectedItem.submitted_at).format('YYYY-MM-DD HH:mm') : 'N/A'}</p>
<p><strong>Decided At:</strong> {selectedItem.decided_at ? moment(selectedItem.decided_at).format('YYYY-MM-DD HH:mm') : 'N/A'}</p>
<p><strong>Reason:</strong> {selectedItem.reason}</p>
<p><strong>Manager Note:</strong> {selectedItem.manager_note}</p>
<p><strong>External Reference:</strong> {selectedItem.external_reference}</p>
</div>
)}
</CardBoxModal>
{!showGrid && kanbanColumns && ( {!showGrid && kanbanColumns && (

View File

@ -1,29 +1,31 @@
import React from 'react'; import React from 'react';
import BaseIcon from '../BaseIcon';
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
import axios from 'axios'; import axios from 'axios';
import { import {
GridActionsCellItem,
GridRowParams,
GridValueGetterParams, GridValueGetterParams,
} from '@mui/x-data-grid'; } 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 ListActionsPopover from '../ListActionsPopover';
import {hasPermission} from "../../helpers/userPermissions"; import {hasPermission} from "../../helpers/userPermissions";
type Params = (id: string) => void; 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 ( export const loadColumns = async (
onDelete: Params, onDelete: Params,
entityName: string, entityName: string,
user, user,
onCancel?: Params onCancel?: Params,
onView?: (row: any) => void
) => { ) => {
async function callOptionsApi(entityName: string) { async function callOptionsApi(entityName: string) {
@ -39,18 +41,26 @@ export const loadColumns = async (
} }
const hasUpdatePermission = hasPermission(user, 'UPDATE_TIME_OFF_REQUESTS') const hasUpdatePermission = hasPermission(user, 'UPDATE_TIME_OFF_REQUESTS')
const isAdmin = user?.app_role?.name === 'Administrator';
return [ const columns: any[] = [
{ {
field: 'requester', field: 'requester',
headerName: 'Requester', headerName: 'Requester',
flex: 1, flex: 1,
minWidth: 120, minWidth: 150,
filterable: false, filterable: false,
headerClassName: 'datagrid--header', headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell', cellClassName: 'datagrid--cell',
renderCell: (params: GridValueGetterParams) => (
<span
className="cursor-pointer text-blue-600 hover:underline font-medium"
onClick={() => onView && onView(params.row)}
>
{getUserLabel(params?.row?.requester)}
</span>
),
editable: hasUpdatePermission, editable: hasUpdatePermission,
@ -68,11 +78,15 @@ export const loadColumns = async (
field: 'approver', field: 'approver',
headerName: 'Approver', headerName: 'Approver',
flex: 1, flex: 1,
minWidth: 120, minWidth: 150,
filterable: false, filterable: false,
headerClassName: 'datagrid--header', headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell', cellClassName: 'datagrid--cell',
renderCell: (params: GridValueGetterParams) => (
<span>
{getUserLabel(params?.row?.approver)}
</span>
),
editable: hasUpdatePermission, editable: hasUpdatePermission,
@ -88,7 +102,7 @@ export const loadColumns = async (
{ {
field: 'leave_type', field: 'leave_type',
headerName: 'LeaveType', headerName: 'Type',
flex: 1, flex: 1,
minWidth: 120, minWidth: 120,
filterable: false, filterable: false,
@ -109,7 +123,7 @@ export const loadColumns = async (
filterable: false, filterable: false,
headerClassName: 'datagrid--header', headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell', cellClassName: 'datagrid--cell',
hide: !isAdmin,
editable: hasUpdatePermission, editable: hasUpdatePermission,
@ -118,7 +132,7 @@ export const loadColumns = async (
{ {
field: 'starts_at', field: 'starts_at',
headerName: 'StartsAt', headerName: 'Start',
flex: 1, flex: 1,
minWidth: 120, minWidth: 120,
filterable: false, filterable: false,
@ -136,7 +150,7 @@ export const loadColumns = async (
{ {
field: 'ends_at', field: 'ends_at',
headerName: 'EndsAt', headerName: 'Finish',
flex: 1, flex: 1,
minWidth: 120, minWidth: 120,
filterable: false, filterable: false,
@ -160,7 +174,7 @@ export const loadColumns = async (
filterable: false, filterable: false,
headerClassName: 'datagrid--header', headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell', cellClassName: 'datagrid--cell',
hide: true,
editable: hasUpdatePermission, editable: hasUpdatePermission,
@ -207,7 +221,7 @@ export const loadColumns = async (
filterable: false, filterable: false,
headerClassName: 'datagrid--header', headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell', cellClassName: 'datagrid--cell',
hide: true, // Hidden by default
editable: hasUpdatePermission, editable: hasUpdatePermission,
@ -223,7 +237,7 @@ export const loadColumns = async (
filterable: false, filterable: false,
headerClassName: 'datagrid--header', headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell', cellClassName: 'datagrid--cell',
hide: true, // Hidden by default
editable: hasUpdatePermission, editable: hasUpdatePermission,
@ -241,7 +255,7 @@ export const loadColumns = async (
filterable: false, filterable: false,
headerClassName: 'datagrid--header', headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell', cellClassName: 'datagrid--cell',
hide: true, // Hidden by default
editable: hasUpdatePermission, editable: hasUpdatePermission,
@ -259,7 +273,7 @@ export const loadColumns = async (
filterable: false, filterable: false,
headerClassName: 'datagrid--header', headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell', cellClassName: 'datagrid--cell',
hide: true, // Hidden by default
editable: hasUpdatePermission, editable: hasUpdatePermission,
@ -274,7 +288,7 @@ export const loadColumns = async (
filterable: false, filterable: false,
headerClassName: 'datagrid--header', headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell', cellClassName: 'datagrid--cell',
hide: true, // Hidden by default
editable: hasUpdatePermission, editable: hasUpdatePermission,
@ -289,22 +303,9 @@ export const loadColumns = async (
filterable: false, filterable: false,
headerClassName: 'datagrid--header', headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell', cellClassName: 'datagrid--cell',
hide: true,
editable: false, editable: false,
sortable: false, sortable: false,
renderCell: (params: GridValueGetterParams) => (
<>
{dataFormatter.filesFormatter(params.row.attachments).map(link => (
<button
key={link.publicUrl}
onClick={(e) => saveFile(e, link.publicUrl, link.name)}
>
{link.name}
</button>
))}
</>
),
}, },
{ {
@ -315,7 +316,7 @@ export const loadColumns = async (
filterable: false, filterable: false,
headerClassName: 'datagrid--header', headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell', cellClassName: 'datagrid--cell',
hide: true,
editable: hasUpdatePermission, editable: hasUpdatePermission,
@ -328,7 +329,8 @@ export const loadColumns = async (
minWidth: 30, minWidth: 30,
headerClassName: 'datagrid--header', headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell', cellClassName: 'datagrid--cell',
getActions: (params: GridRowParams) => { hide: true,
getActions: (params: any) => {
return [ return [
<div key={params?.row?.id}> <div key={params?.row?.id}>
@ -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;
});
}; };

View File

@ -1,6 +1,5 @@
import { mdiChartTimelineVariant } from '@mdi/js' import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head' import Head from 'next/head'
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useState } from 'react'
import CardBox from '../../components/CardBox' import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated' import LayoutAuthenticated from '../../layouts/Authenticated'
@ -27,6 +26,7 @@ const Time_off_requestsTablesPage = () => {
const [csvFile, setCsvFile] = useState<File | null>(null); const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false); const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(true); const [showTableView, setShowTableView] = useState(true);
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth); const { currentUser } = useAppSelector((state) => state.auth);
@ -35,10 +35,10 @@ const Time_off_requestsTablesPage = () => {
const dispatch = useAppDispatch(); 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: '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'}, {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'); const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_TIME_OFF_REQUESTS');
@ -62,27 +62,13 @@ const Time_off_requestsTablesPage = () => {
}, undefined, { shallow: true }); }, 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 getTime_off_requestsCSV = async () => {
const response = await axios({url: '/time_off_requests?filetype=csv', method: 'GET',responseType: 'blob'}); const response = await axios({url: '/time_off_requests?filetype=csv', method: 'GET',responseType: 'blob'});
const type = response.headers['content-type'] const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type }) const blob = new Blob([response.data], { type: type })
const link = document.createElement('a') const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob) link.href = window.URL.createObjectURL(blob)
link.download = 'time_off_requestsCSV.csv' link.download = 'time_off_requests_export.csv'
link.click() link.click()
}; };
@ -102,10 +88,10 @@ const Time_off_requestsTablesPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Time_off_requests')}</title> <title>{getPageTitle('Time Off Requests')}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Time_off_requests" main> <SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Time Off Requests" main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'> <CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
@ -116,7 +102,7 @@ const Time_off_requestsTablesPage = () => {
className={'mr-3'} className={'mr-3'}
color='info' color='info'
label='Filter' label='Filter'
onClick={addFilter} onClick={() => setIsFilterDrawerOpen(true)}
/> />
<BaseButton <BaseButton
className={'mr-3'} className={'mr-3'}
@ -124,10 +110,11 @@ const Time_off_requestsTablesPage = () => {
label='Pending' label='Pending'
onClick={showPending} onClick={showPending}
/> />
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getTime_off_requestsCSV} /> <BaseButton className={'mr-3'} color='info' label='Export' onClick={getTime_off_requestsCSV} />
{hasCreatePermission && ( {hasCreatePermission && (
<BaseButton <BaseButton
className={'mr-3'}
color='info' color='info'
label='Upload CSV' label='Upload CSV'
onClick={() => setIsModalActive(true)} onClick={() => setIsModalActive(true)}
@ -153,6 +140,8 @@ const Time_off_requestsTablesPage = () => {
setFilterItems={setFilterItems} setFilterItems={setFilterItems}
filters={filters} filters={filters}
showGrid={showTableView} showGrid={showTableView}
isFilterDrawerOpen={isFilterDrawerOpen}
setIsFilterDrawerOpen={setIsFilterDrawerOpen}
/> />
</SectionMain> </SectionMain>