diff --git a/backend/src/db/api/time_off_requests.js b/backend/src/db/api/time_off_requests.js index 14e6ac4..0331939 100644 --- a/backend/src/db/api/time_off_requests.js +++ b/backend/src/db/api/time_off_requests.js @@ -445,7 +445,7 @@ module.exports = class Time_off_requestsDBApi { { model: db.users, as: 'requester', - required: false, + required: !!filter.requester, where: filter.requester ? { [Op.or]: [ { id: { [Op.in]: filter.requester.split('|').map(term => Utils.uuid(term)) } }, @@ -454,6 +454,11 @@ module.exports = class Time_off_requestsDBApi { [Op.or]: filter.requester.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) } }, + { + lastName: { + [Op.or]: filter.requester.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) + } + }, ] } : undefined, @@ -462,7 +467,7 @@ module.exports = class Time_off_requestsDBApi { { model: db.users, as: 'approver', - required: false, + required: !!filter.approver, where: filter.approver ? { [Op.or]: [ { id: { [Op.in]: filter.approver.split('|').map(term => Utils.uuid(term)) } }, @@ -471,6 +476,11 @@ module.exports = class Time_off_requestsDBApi { [Op.or]: filter.approver.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) } }, + { + lastName: { + [Op.or]: filter.approver.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) + } + }, ] } : undefined, @@ -533,7 +543,8 @@ module.exports = class Time_off_requestsDBApi { if (filter.starts_atRange) { - const [start, end] = filter.starts_atRange; + const range = Array.isArray(filter.starts_atRange) ? filter.starts_atRange : [filter.starts_atRange]; + const [start, end] = range; if (start !== undefined && start !== null && start !== '') { where = { @@ -557,7 +568,8 @@ module.exports = class Time_off_requestsDBApi { } if (filter.ends_atRange) { - const [start, end] = filter.ends_atRange; + const range = Array.isArray(filter.ends_atRange) ? filter.ends_atRange : [filter.ends_atRange]; + const [start, end] = range; if (start !== undefined && start !== null && start !== '') { where = { @@ -581,7 +593,8 @@ module.exports = class Time_off_requestsDBApi { } if (filter.hoursRange) { - const [start, end] = filter.hoursRange; + const range = Array.isArray(filter.hoursRange) ? filter.hoursRange : [filter.hoursRange]; + const [start, end] = range; if (start !== undefined && start !== null && start !== '') { where = { @@ -605,7 +618,8 @@ module.exports = class Time_off_requestsDBApi { } if (filter.daysRange) { - const [start, end] = filter.daysRange; + const range = Array.isArray(filter.daysRange) ? filter.daysRange : [filter.daysRange]; + const [start, end] = range; if (start !== undefined && start !== null && start !== '') { where = { @@ -629,7 +643,8 @@ module.exports = class Time_off_requestsDBApi { } if (filter.submitted_atRange) { - const [start, end] = filter.submitted_atRange; + const range = Array.isArray(filter.submitted_atRange) ? filter.submitted_atRange : [filter.submitted_atRange]; + const [start, end] = range; if (start !== undefined && start !== null && start !== '') { where = { @@ -653,7 +668,8 @@ module.exports = class Time_off_requestsDBApi { } if (filter.decided_atRange) { - const [start, end] = filter.decided_atRange; + const range = Array.isArray(filter.decided_atRange) ? filter.decided_atRange : [filter.decided_atRange]; + const [start, end] = range; if (start !== undefined && start !== null && start !== '') { where = { @@ -729,7 +745,8 @@ module.exports = class Time_off_requestsDBApi { if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; + const range = Array.isArray(filter.createdAtRange) ? filter.createdAtRange : [filter.createdAtRange]; + const [start, end] = range; if (start !== undefined && start !== null && start !== '') { where = { @@ -828,4 +845,4 @@ module.exports = class Time_off_requestsDBApi { } -}; +}; \ No newline at end of file diff --git a/backend/src/services/time_off_requests.js b/backend/src/services/time_off_requests.js index 1a9561e..93a690b 100644 --- a/backend/src/services/time_off_requests.js +++ b/backend/src/services/time_off_requests.js @@ -64,8 +64,8 @@ module.exports = class Time_off_requestsService { }, ); - // Create calendar event if approved - if (createdRequest.status === 'approved') { + // Create calendar event if approved or pending_approval + if (['approved', 'pending_approval'].includes(createdRequest.status)) { await Office_calendar_eventsDBApi.create({ event_type: 'time_off', title: `PTO - ${requester?.firstName || ''} ${requester?.lastName || ''}`, @@ -220,8 +220,12 @@ module.exports = class Time_off_requestsService { }, ); - // Create calendar event if status changed to approved - if (newStatus === 'approved' && oldStatus !== 'approved') { + // Create/Update/Delete calendar event based on status + const visibleStatuses = ['approved', 'pending_approval']; + const isVisibleNow = visibleStatuses.includes(newStatus); + const wasVisibleBefore = visibleStatuses.includes(oldStatus); + + if (isVisibleNow && !wasVisibleBefore) { const requester = await db.users.findByPk(updatedTime_off_requests.requesterId, { transaction }); await Office_calendar_eventsDBApi.create({ event_type: 'time_off', @@ -232,21 +236,23 @@ module.exports = class Time_off_requestsService { time_off_request: updatedTime_off_requests.id, is_all_day: true }, { currentUser, transaction }); - } else if (newStatus !== 'approved' && oldStatus === 'approved') { - // Delete calendar event if no longer approved + } else if (!isVisibleNow && wasVisibleBefore) { + // Delete calendar event if no longer visible await db.office_calendar_events.destroy({ where: { time_off_requestId: id }, transaction }); - } else if (newStatus === 'approved' && (data.starts_at || data.ends_at)) { + } else if (isVisibleNow && wasVisibleBefore) { // Update calendar event if dates changed - await db.office_calendar_events.update({ - starts_at: updatedTime_off_requests.starts_at, - ends_at: updatedTime_off_requests.ends_at - }, { - where: { time_off_requestId: id }, - transaction - }); + if (data.starts_at || data.ends_at) { + await db.office_calendar_events.update({ + starts_at: updatedTime_off_requests.starts_at, + ends_at: updatedTime_off_requests.ends_at + }, { + where: { time_off_requestId: id }, + transaction + }); + } } // Handle cancellation: dismiss associated approval tasks diff --git a/frontend/src/components/BigCalendar.tsx b/frontend/src/components/BigCalendar.tsx index 7f22f66..ae7c45c 100644 --- a/frontend/src/components/BigCalendar.tsx +++ b/frontend/src/components/BigCalendar.tsx @@ -25,6 +25,7 @@ type TEvent = { event_type?: string; user?: any; holiday?: any; + time_off_request?: any; }; type Props = { @@ -147,8 +148,12 @@ const BigCalendar = ({ const color = 'white'; if (entityName === 'office_calendar_events') { - if (event.event_type === 'time_off' && event.user) { - backgroundColor = stringToColor(event.user.id); + if (event.event_type === 'time_off') { + if (event.time_off_request?.status === 'approved') { + backgroundColor = '#3b82f6'; // Blue + } else { + backgroundColor = '#eab308'; // Yellow + } } else if (event.event_type === 'holiday') { backgroundColor = '#f0ad4e'; } else { @@ -230,4 +235,4 @@ const MyCustomEvent = ( ); }; -export default BigCalendar; +export default BigCalendar; \ No newline at end of file diff --git a/frontend/src/components/Pto_journal_entries/configurePto_journal_entriesCols.tsx b/frontend/src/components/Pto_journal_entries/configurePto_journal_entriesCols.tsx index d3e10d7..80474b9 100644 --- a/frontend/src/components/Pto_journal_entries/configurePto_journal_entriesCols.tsx +++ b/frontend/src/components/Pto_journal_entries/configurePto_journal_entriesCols.tsx @@ -14,9 +14,19 @@ import DataGridMultiSelect from "../DataGridMultiSelect"; import ListActionsPopover from '../ListActionsPopover'; import {hasPermission} from "../../helpers/userPermissions"; +import { leaveTypeLabels } from '../Time_off_requests/configureTime_off_requestsCols'; 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, @@ -45,11 +55,15 @@ export const loadColumns = async ( field: 'user', headerName: 'User', flex: 1, - minWidth: 120, + minWidth: 150, filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + renderCell: (params: GridValueGetterParams) => ( + + {getUserLabel(params?.row?.user)} + + ), editable: hasUpdatePermission, @@ -65,9 +79,9 @@ export const loadColumns = async ( { field: 'source_request', - headerName: 'SourceRequest', + headerName: 'Source Request', flex: 1, - minWidth: 120, + minWidth: 150, filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', @@ -87,7 +101,7 @@ export const loadColumns = async ( { field: 'entry_type', - headerName: 'EntryType', + headerName: 'Entry Type', flex: 1, minWidth: 120, filterable: false, @@ -102,13 +116,17 @@ export const loadColumns = async ( { field: 'leave_bucket', - headerName: 'LeaveBucket', + headerName: 'Leave Bucket', flex: 1, - minWidth: 120, + minWidth: 150, filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + renderCell: (params: GridValueGetterParams) => ( + + {leaveTypeLabels[params.value] || params.value} + + ), editable: hasUpdatePermission, @@ -117,7 +135,7 @@ export const loadColumns = async ( { field: 'effective_at', - headerName: 'EffectiveAt', + headerName: 'Effective At', flex: 1, minWidth: 120, filterable: false, @@ -135,7 +153,7 @@ export const loadColumns = async ( { field: 'calendar_year', - headerName: 'CalendarYear', + headerName: 'Calendar Year', flex: 1, minWidth: 120, filterable: false, @@ -151,7 +169,7 @@ export const loadColumns = async ( { field: 'amount_hours', - headerName: 'AmountHours', + headerName: 'Amount Hours', flex: 1, minWidth: 120, filterable: false, @@ -167,7 +185,7 @@ export const loadColumns = async ( { field: 'amount_days', - headerName: 'AmountDays', + headerName: 'Amount Days', flex: 1, minWidth: 120, filterable: false, @@ -183,7 +201,7 @@ export const loadColumns = async ( { field: 'counts_against_balance', - headerName: 'CountsAgainstBalance', + headerName: 'Counts Against Balance', flex: 1, minWidth: 120, filterable: false, @@ -199,7 +217,7 @@ export const loadColumns = async ( { field: 'posting_status', - headerName: 'PostingStatus', + headerName: 'Posting Status', flex: 1, minWidth: 120, filterable: false, @@ -229,13 +247,17 @@ export const loadColumns = async ( { field: 'entered_by', - headerName: 'EnteredBy', + headerName: 'Entered By', flex: 1, - minWidth: 120, + minWidth: 150, filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + renderCell: (params: GridValueGetterParams) => ( + + {getUserLabel(params?.row?.entered_by)} + + ), editable: hasUpdatePermission, @@ -251,7 +273,7 @@ export const loadColumns = async ( { field: 'entered_at', - headerName: 'EnteredAt', + headerName: 'Entered At', flex: 1, minWidth: 120, filterable: false, @@ -291,4 +313,4 @@ export const loadColumns = async ( }, }, ]; -}; +}; \ No newline at end of file diff --git a/frontend/src/components/StaffOffList.tsx b/frontend/src/components/StaffOffList.tsx index ebce5b5..041c01c 100644 --- a/frontend/src/components/StaffOffList.tsx +++ b/frontend/src/components/StaffOffList.tsx @@ -3,7 +3,7 @@ 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'; +import { mdiArrowLeft, mdiArrowRight, mdiCalendarCheck, mdiAlertCircle, mdiDoctor, mdiClockOutline } from '@mdi/js'; interface TimeOffRequest { id: string; @@ -49,10 +49,11 @@ export default function StaffOffList() { // Filter client-side for status/type criteria const filtered = response.data.rows.filter((r: any) => { const isApproved = r.status === 'approved'; + const isPending = r.status === 'pending_approval'; const isSpecialType = ['unplanned_pto', 'medical_leave'].includes(r.leave_type); - // Include if approved OR (unplanned/medical and not rejected/cancelled) + // Include if approved OR pending OR (unplanned/medical and not rejected/cancelled) const isActive = !['rejected', 'cancelled'].includes(r.status); - return (isApproved || (isSpecialType && isActive)); + return (isApproved || isPending || (isSpecialType && isActive)); }); setRequests(filtered); @@ -70,16 +71,18 @@ export default function StaffOffList() { const prevWeek = () => setWeekStart(weekStart.clone().subtract(1, 'weeks')); const nextWeek = () => setWeekStart(weekStart.clone().add(1, 'weeks')); - const getIcon = (type: string) => { - switch (type) { + const getIcon = (r: TimeOffRequest) => { + if (r.status === 'pending_approval') return mdiClockOutline; + switch (r.leave_type) { case 'medical_leave': return mdiDoctor; case 'unplanned_pto': return mdiAlertCircle; default: return mdiCalendarCheck; } }; - const getColor = (type: string) => { - switch (type) { + const getColor = (r: TimeOffRequest) => { + if (r.status === 'pending_approval') return 'text-yellow-500'; + switch (r.leave_type) { case 'medical_leave': return 'text-red-500'; case 'unplanned_pto': return 'text-orange-500'; default: return 'text-green-500'; @@ -117,9 +120,9 @@ export default function StaffOffList() {
{r.requester?.firstName} {r.requester?.lastName}
-
- - {r.leave_type.replace('_', ' ')} +
+ + {r.leave_type.replace('_', ' ')} {r.status === 'pending_approval' ? '(Pending)' : ''}
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 c2c0a8d..64eccd3 100644 --- a/frontend/src/components/Time_off_requests/TableTime_off_requests.tsx +++ b/frontend/src/components/Time_off_requests/TableTime_off_requests.tsx @@ -15,7 +15,7 @@ import { 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, leaveTypeLabels} from "./configureTime_off_requestsCols"; import _ from 'lodash'; import dataFormatter from '../../helpers/dataFormatter' import {dataGridStyles} from "../../styles"; @@ -231,12 +231,10 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh ); if (isRangeFilter) { - const from = item.fields.filterValueFrom; - const to = item.fields.filterValueTo; - if (from) { + const from = item.fields.filterValueFrom || ""; + const to = item.fields.filterValueTo || ""; + if (from || to) { request += `${item.fields.selectedField}Range=${from}&`; - } - if (to) { request += `${item.fields.selectedField}Range=${to}&`; } } else { @@ -494,8 +492,8 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh > {selectedFilter?.options?.map((option) => ( - ))} @@ -628,7 +626,7 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh

Requester: {getUserLabel(selectedItem.requester)}

Approver: {getUserLabel(selectedItem.approver)}

-

Type: {selectedItem.leave_type}

+

Type: {leaveTypeLabels[selectedItem.leave_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')}

@@ -679,4 +677,4 @@ const TableSampleTime_off_requests = ({ filterItems, setFilterItems, filters, sh ) } -export default TableSampleTime_off_requests \ No newline at end of file +export default TableSampleTime_off_requests; \ No newline at end of file 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 224eefb..9c90825 100644 --- a/frontend/src/components/Time_off_requests/configureTime_off_requestsCols.tsx +++ b/frontend/src/components/Time_off_requests/configureTime_off_requestsCols.tsx @@ -18,6 +18,14 @@ const getUserLabel = (user: any) => { return user.id || ''; } +export const leaveTypeLabels = { + regular_pto: 'PTO Planned', + medical_leave: 'Medical', + unplanned_pto: 'PTO Unplanned', + bereavement: 'Bereavement', + time_in_lieu: 'Time in Lieu' +}; + export const loadColumns = async ( onDelete: Params, entityName: string, @@ -108,7 +116,11 @@ export const loadColumns = async ( filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + renderCell: (params: GridValueGetterParams) => ( + + {leaveTypeLabels[params.value] || params.value} + + ), editable: hasUpdatePermission, diff --git a/frontend/src/components/Yearly_leave_summaries/configureYearly_leave_summariesCols.tsx b/frontend/src/components/Yearly_leave_summaries/configureYearly_leave_summariesCols.tsx index 5518b3d..66db0a4 100644 --- a/frontend/src/components/Yearly_leave_summaries/configureYearly_leave_summariesCols.tsx +++ b/frontend/src/components/Yearly_leave_summaries/configureYearly_leave_summariesCols.tsx @@ -17,6 +17,15 @@ 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, @@ -45,11 +54,15 @@ export const loadColumns = async ( field: 'user', headerName: 'User', flex: 1, - minWidth: 120, + minWidth: 150, filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - + renderCell: (params: GridValueGetterParams) => ( + + {getUserLabel(params?.row?.user)} + + ), editable: hasUpdatePermission, @@ -65,7 +78,7 @@ export const loadColumns = async ( { field: 'calendar_year', - headerName: 'CalendarYear', + headerName: 'Calendar Year', flex: 1, minWidth: 120, filterable: false, @@ -81,7 +94,7 @@ export const loadColumns = async ( { field: 'pto_pending_days', - headerName: 'PTOPendingDays', + headerName: 'PTO Pending', flex: 1, minWidth: 120, filterable: false, @@ -97,7 +110,7 @@ export const loadColumns = async ( { field: 'pto_scheduled_days', - headerName: 'PTOScheduledDays', + headerName: 'PTO Scheduled', flex: 1, minWidth: 120, filterable: false, @@ -113,7 +126,7 @@ export const loadColumns = async ( { field: 'pto_taken_days', - headerName: 'PTOTakenDays', + headerName: 'PTO Taken', flex: 1, minWidth: 120, filterable: false, @@ -129,7 +142,7 @@ export const loadColumns = async ( { field: 'pto_available_days', - headerName: 'PTOAvailableDays', + headerName: 'PTO Available', flex: 1, minWidth: 120, filterable: false, @@ -145,7 +158,7 @@ export const loadColumns = async ( { field: 'medical_taken_days', - headerName: 'MedicalTakenDays', + headerName: 'Medical Taken', flex: 1, minWidth: 120, filterable: false, @@ -161,7 +174,7 @@ export const loadColumns = async ( { field: 'bereavement_taken_days', - headerName: 'BereavementTakenDays', + headerName: 'Bereavement Taken', flex: 1, minWidth: 120, filterable: false, @@ -177,7 +190,7 @@ export const loadColumns = async ( { field: 'time_in_lieu_available_days', - headerName: 'TimeInLieuAvailableDays', + headerName: 'Time In Lieu Available', flex: 1, minWidth: 120, filterable: false, @@ -193,7 +206,7 @@ export const loadColumns = async ( { field: 'vacation_pay_paid_amount', - headerName: 'VacationPayPaidAmount', + headerName: 'Vacation Pay Paid', flex: 1, minWidth: 120, filterable: false, @@ -255,4 +268,4 @@ export const loadColumns = async ( }, }, ]; -}; \ No newline at end of file +}; diff --git a/frontend/src/pages/pto_journal_entries/pto_journal_entries-view.tsx b/frontend/src/pages/pto_journal_entries/pto_journal_entries-view.tsx index f8bcd81..4f08d77 100644 --- a/frontend/src/pages/pto_journal_entries/pto_journal_entries-view.tsx +++ b/frontend/src/pages/pto_journal_entries/pto_journal_entries-view.tsx @@ -19,6 +19,7 @@ import BaseDivider from "../../components/BaseDivider"; import {mdiChartTimelineVariant} from "@mdi/js"; import {SwitchField} from "../../components/SwitchField"; import FormField from "../../components/FormField"; +import { leaveTypeLabels } from '../../components/Time_off_requests/configureTime_off_requestsCols'; const Pto_journal_entriesView = () => { @@ -26,6 +27,14 @@ const Pto_journal_entriesView = () => { const dispatch = useAppDispatch() const { pto_journal_entries } = useAppSelector((state) => state.pto_journal_entries) + 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 { id } = router.query; @@ -42,10 +51,10 @@ const Pto_journal_entriesView = () => { return ( <> - {getPageTitle('View pto_journal_entries')} + {getPageTitle('View Time Off Entry')} - + {

User

-

{pto_journal_entries?.user?.firstName ?? 'No data'}

+

{getUserLabel(pto_journal_entries?.user) || 'No data'}

@@ -132,7 +141,7 @@ const Pto_journal_entriesView = () => {
-

SourceRequest

+

Source Request

@@ -183,7 +192,7 @@ const Pto_journal_entriesView = () => {
-

EntryType

+

Entry Type

{pto_journal_entries?.entry_type ?? 'No data'}

@@ -215,8 +224,8 @@ const Pto_journal_entriesView = () => {
-

LeaveBucket

-

{pto_journal_entries?.leave_bucket ?? 'No data'}

+

Leave Bucket

+

{leaveTypeLabels[pto_journal_entries?.leave_bucket] || pto_journal_entries?.leave_bucket || 'No data'}

@@ -242,7 +251,7 @@ const Pto_journal_entriesView = () => { - + {pto_journal_entries.effective_at ? { ) : null } disabled - /> :

No EffectiveAt

} + /> :

No Effective At

}
@@ -279,7 +288,7 @@ const Pto_journal_entriesView = () => {
-

CalendarYear

+

Calendar Year

{pto_journal_entries?.calendar_year || 'No data'}

@@ -311,7 +320,7 @@ const Pto_journal_entriesView = () => {
-

AmountHours

+

Amount Hours

{pto_journal_entries?.amount_hours || 'No data'}

@@ -343,7 +352,7 @@ const Pto_journal_entriesView = () => {
-

AmountDays

+

Amount Days

{pto_journal_entries?.amount_days || 'No data'}

@@ -384,7 +393,7 @@ const Pto_journal_entriesView = () => { - + null}} @@ -418,7 +427,7 @@ const Pto_journal_entriesView = () => {
-

PostingStatus

+

Posting Status

{pto_journal_entries?.posting_status ?? 'No data'}

@@ -437,7 +446,7 @@ const Pto_journal_entriesView = () => { - +