From 222adbd50a602e2739b51461b99b49d4d3c630cb Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 12 Dec 2025 08:26:32 +1100 Subject: [PATCH 01/11] Updates for local routing --- .../admin/src/lib/services/reports.service.ts | 2 +- src/apps/leave-tracker/index.ts | 1 + .../leave-tracker/src/LeaveTrackerApp.tsx | 33 ++++ .../leave-tracker/src/config/index.config.ts | 4 + .../leave-tracker/src/config/routes.config.ts | 9 ++ src/apps/leave-tracker/src/index.ts | 2 + .../src/leave-tracker-app.routes.tsx | 39 +++++ .../components/Calendar/Calendar.module.scss | 116 ++++++++++++++ .../src/lib/components/Calendar/Calendar.tsx | 110 +++++++++++++ .../src/lib/components/Calendar/index.ts | 1 + .../CalendarLegend/CalendarLegend.module.scss | 42 +++++ .../CalendarLegend/CalendarLegend.tsx | 19 +++ .../lib/components/CalendarLegend/index.ts | 1 + .../lib/components/Layout/Layout.module.scss | 55 +++++++ .../src/lib/components/Layout/Layout.tsx | 59 +++++++ .../src/lib/components/Layout/index.ts | 1 + .../MonthNavigation.module.scss | 32 ++++ .../MonthNavigation/MonthNavigation.tsx | 40 +++++ .../lib/components/MonthNavigation/index.ts | 1 + .../TeamCalendar/TeamCalendar.module.scss | 151 ++++++++++++++++++ .../components/TeamCalendar/TeamCalendar.tsx | 120 ++++++++++++++ .../src/lib/components/TeamCalendar/index.ts | 1 + .../leave-tracker/src/lib/components/index.ts | 5 + .../src/lib/contexts/LeaveTrackerContext.ts | 9 ++ .../contexts/LeaveTrackerContextProvider.tsx | 40 +++++ .../src/lib/contexts/SWRConfigProvider.tsx | 19 +++ .../leave-tracker/src/lib/contexts/index.ts | 3 + src/apps/leave-tracker/src/lib/hooks/index.ts | 2 + .../src/lib/hooks/useFetchLeaveDates.ts | 91 +++++++++++ .../src/lib/hooks/useFetchTeamLeave.ts | 62 +++++++ src/apps/leave-tracker/src/lib/index.ts | 6 + .../leave-tracker/src/lib/models/index.ts | 28 ++++ .../leave-tracker/src/lib/services/index.ts | 1 + .../src/lib/services/leave.service.ts | 32 ++++ .../leave-tracker/src/lib/styles/index.scss | 37 +++++ .../src/lib/utils/calendar.utils.ts | 56 +++++++ src/apps/leave-tracker/src/lib/utils/index.ts | 1 + .../PersonalCalendarPage.module.scss | 77 +++++++++ .../PersonalCalendarPage.tsx | 151 ++++++++++++++++++ .../src/pages/personal-calendar/index.ts | 1 + .../TeamCalendarPage.module.scss | 46 ++++++ .../pages/team-calendar/TeamCalendarPage.tsx | 73 +++++++++ .../src/pages/team-calendar/index.ts | 1 + src/apps/platform/src/platform.routes.tsx | 2 + src/apps/review/src/lib/hooks/useRole.ts | 106 +++++++----- src/config/constants.ts | 2 + src/config/environments/local.env.ts | 2 + .../profile-factory/user-role.enum.ts | 3 +- 48 files changed, 1652 insertions(+), 43 deletions(-) create mode 100644 src/apps/leave-tracker/index.ts create mode 100644 src/apps/leave-tracker/src/LeaveTrackerApp.tsx create mode 100644 src/apps/leave-tracker/src/config/index.config.ts create mode 100644 src/apps/leave-tracker/src/config/routes.config.ts create mode 100644 src/apps/leave-tracker/src/index.ts create mode 100644 src/apps/leave-tracker/src/leave-tracker-app.routes.tsx create mode 100644 src/apps/leave-tracker/src/lib/components/Calendar/Calendar.module.scss create mode 100644 src/apps/leave-tracker/src/lib/components/Calendar/Calendar.tsx create mode 100644 src/apps/leave-tracker/src/lib/components/Calendar/index.ts create mode 100644 src/apps/leave-tracker/src/lib/components/CalendarLegend/CalendarLegend.module.scss create mode 100644 src/apps/leave-tracker/src/lib/components/CalendarLegend/CalendarLegend.tsx create mode 100644 src/apps/leave-tracker/src/lib/components/CalendarLegend/index.ts create mode 100644 src/apps/leave-tracker/src/lib/components/Layout/Layout.module.scss create mode 100644 src/apps/leave-tracker/src/lib/components/Layout/Layout.tsx create mode 100644 src/apps/leave-tracker/src/lib/components/Layout/index.ts create mode 100644 src/apps/leave-tracker/src/lib/components/MonthNavigation/MonthNavigation.module.scss create mode 100644 src/apps/leave-tracker/src/lib/components/MonthNavigation/MonthNavigation.tsx create mode 100644 src/apps/leave-tracker/src/lib/components/MonthNavigation/index.ts create mode 100644 src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.module.scss create mode 100644 src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.tsx create mode 100644 src/apps/leave-tracker/src/lib/components/TeamCalendar/index.ts create mode 100644 src/apps/leave-tracker/src/lib/components/index.ts create mode 100644 src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContext.ts create mode 100644 src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContextProvider.tsx create mode 100644 src/apps/leave-tracker/src/lib/contexts/SWRConfigProvider.tsx create mode 100644 src/apps/leave-tracker/src/lib/contexts/index.ts create mode 100644 src/apps/leave-tracker/src/lib/hooks/index.ts create mode 100644 src/apps/leave-tracker/src/lib/hooks/useFetchLeaveDates.ts create mode 100644 src/apps/leave-tracker/src/lib/hooks/useFetchTeamLeave.ts create mode 100644 src/apps/leave-tracker/src/lib/index.ts create mode 100644 src/apps/leave-tracker/src/lib/models/index.ts create mode 100644 src/apps/leave-tracker/src/lib/services/index.ts create mode 100644 src/apps/leave-tracker/src/lib/services/leave.service.ts create mode 100644 src/apps/leave-tracker/src/lib/styles/index.scss create mode 100644 src/apps/leave-tracker/src/lib/utils/calendar.utils.ts create mode 100644 src/apps/leave-tracker/src/lib/utils/index.ts create mode 100644 src/apps/leave-tracker/src/pages/personal-calendar/PersonalCalendarPage.module.scss create mode 100644 src/apps/leave-tracker/src/pages/personal-calendar/PersonalCalendarPage.tsx create mode 100644 src/apps/leave-tracker/src/pages/personal-calendar/index.ts create mode 100644 src/apps/leave-tracker/src/pages/team-calendar/TeamCalendarPage.module.scss create mode 100644 src/apps/leave-tracker/src/pages/team-calendar/TeamCalendarPage.tsx create mode 100644 src/apps/leave-tracker/src/pages/team-calendar/index.ts diff --git a/src/apps/admin/src/lib/services/reports.service.ts b/src/apps/admin/src/lib/services/reports.service.ts index a0dd1ef24..f2802ff85 100644 --- a/src/apps/admin/src/lib/services/reports.service.ts +++ b/src/apps/admin/src/lib/services/reports.service.ts @@ -36,7 +36,7 @@ const buildReportUrl = (path: string): string => { } export const fetchReportsIndex = async (): Promise => ( - xhrGetAsync(`${EnvironmentConfig.API.V6}/reports`) + xhrGetAsync(`${EnvironmentConfig.API.V6}/reports/directory`) ) const downloadReportBlob = async (path: string, accept: string): Promise => { diff --git a/src/apps/leave-tracker/index.ts b/src/apps/leave-tracker/index.ts new file mode 100644 index 000000000..6f39cd49b --- /dev/null +++ b/src/apps/leave-tracker/index.ts @@ -0,0 +1 @@ +export * from './src' diff --git a/src/apps/leave-tracker/src/LeaveTrackerApp.tsx b/src/apps/leave-tracker/src/LeaveTrackerApp.tsx new file mode 100644 index 000000000..a701c9819 --- /dev/null +++ b/src/apps/leave-tracker/src/LeaveTrackerApp.tsx @@ -0,0 +1,33 @@ +import { FC, useContext, useEffect, useMemo } from 'react' +import { Outlet, Routes } from 'react-router-dom' + +import { routerContext, RouterContextData } from '~/libs/core' + +import { Layout, LeaveTrackerContextProvider, SWRConfigProvider } from './lib' +import { toolTitle } from './leave-tracker-app.routes' +import './lib/styles/index.scss' + +const LeaveTrackerApp: FC = () => { + const { getChildRoutes }: RouterContextData = useContext(routerContext) + const childRoutes = useMemo(() => getChildRoutes(toolTitle), [getChildRoutes]) + + useEffect(() => { + document.body.classList.add('leave-tracker-app') + return () => { + document.body.classList.remove('leave-tracker-app') + } + }, []) + + return ( + + + + + {childRoutes} + + + + ) +} + +export default LeaveTrackerApp diff --git a/src/apps/leave-tracker/src/config/index.config.ts b/src/apps/leave-tracker/src/config/index.config.ts new file mode 100644 index 000000000..21fb5a95c --- /dev/null +++ b/src/apps/leave-tracker/src/config/index.config.ts @@ -0,0 +1,4 @@ +/** + * Common leave tracker config constants. + */ +export const APP_NAME = 'Leave Tracker' diff --git a/src/apps/leave-tracker/src/config/routes.config.ts b/src/apps/leave-tracker/src/config/routes.config.ts new file mode 100644 index 000000000..148f14917 --- /dev/null +++ b/src/apps/leave-tracker/src/config/routes.config.ts @@ -0,0 +1,9 @@ +import { AppSubdomain, EnvironmentConfig } from '~/config' + +export const rootRoute: string + = EnvironmentConfig.SUBDOMAIN === AppSubdomain.leaveTracker + ? '' + : `/${AppSubdomain.leaveTracker}` + +export const personalCalendarRouteId = 'personal-calendar' +export const teamCalendarRouteId = 'team-calendar' diff --git a/src/apps/leave-tracker/src/index.ts b/src/apps/leave-tracker/src/index.ts new file mode 100644 index 000000000..59efccaa5 --- /dev/null +++ b/src/apps/leave-tracker/src/index.ts @@ -0,0 +1,2 @@ +export { leaveTrackerRoutes } from './leave-tracker-app.routes' +export { rootRoute as leaveTrackerRootRoute } from './config/routes.config' diff --git a/src/apps/leave-tracker/src/leave-tracker-app.routes.tsx b/src/apps/leave-tracker/src/leave-tracker-app.routes.tsx new file mode 100644 index 000000000..842667b5e --- /dev/null +++ b/src/apps/leave-tracker/src/leave-tracker-app.routes.tsx @@ -0,0 +1,39 @@ +import { AppSubdomain, ToolTitle } from '~/config' +import { lazyLoad, LazyLoadedComponent, PlatformRoute } from '~/libs/core' +import { UserRole } from '~/libs/core/lib/profile/profile-functions/profile-factory/user-role.enum' + +import { personalCalendarRouteId, rootRoute, teamCalendarRouteId } from './config/routes.config' + +const LeaveTrackerApp: LazyLoadedComponent = lazyLoad(() => import('./LeaveTrackerApp')) +const PersonalCalendarPage: LazyLoadedComponent = lazyLoad( + () => import('./pages/personal-calendar'), +) +const TeamCalendarPage: LazyLoadedComponent = lazyLoad(() => import('./pages/team-calendar')) + +export const toolTitle: string = ToolTitle.leaveTracker + +export const leaveTrackerRoutes: ReadonlyArray = [ + { + domain: AppSubdomain.leaveTracker, + element: , + id: toolTitle, + route: rootRoute, + title: toolTitle, + authRequired: true, + rolesRequired: [UserRole.topcoderStaff, UserRole.administrator], + children: [ + { + element: , + id: personalCalendarRouteId, + route: '', + title: 'Personal Calendar', + }, + { + element: , + id: teamCalendarRouteId, + route: 'team-calendar', + title: 'Team Calendar', + }, + ], + }, +] diff --git a/src/apps/leave-tracker/src/lib/components/Calendar/Calendar.module.scss b/src/apps/leave-tracker/src/lib/components/Calendar/Calendar.module.scss new file mode 100644 index 000000000..29891004a --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/Calendar/Calendar.module.scss @@ -0,0 +1,116 @@ +.calendar { + position: relative; + background: #ffffff; + border: 1px solid var(--calendar-border); + border-radius: 12px; + padding: 16px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04); +} + +.dayNames { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 8px; + margin-bottom: 12px; + color: #6c757d; + text-align: center; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.dayName { + padding: 4px 0; +} + +.grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 8px; +} + +.cell { + position: relative; + min-height: 88px; + width: 100%; + border-radius: 10px; + border: 1px solid var(--calendar-border); + background: var(--status-available); + display: flex; + align-items: flex-start; + justify-content: flex-end; + padding: 10px; + cursor: pointer; + transition: background-color 0.12s ease, transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease; + font-weight: 700; + color: #2d2d2d; +} + +.cell:hover { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05); + background: var(--calendar-hover); +} + +.cell:disabled { + cursor: not-allowed; + opacity: 0.75; +} + +.empty { + background: transparent; + border: none; + pointer-events: none; +} + +.dateNumber { + font-size: 16px; + line-height: 1; +} + +.status-available { + background: var(--status-available); +} + +.status-leave { + background: var(--status-leave); + color: #ffffff; +} + +.status-holiday { + background: var(--status-holiday); +} + +.status-weekend { + background: var(--status-weekend); +} + +.selected { + border-color: var(--status-selected); + box-shadow: 0 0 0 2px rgba(77, 171, 247, 0.3); +} + +.loading { + pointer-events: none; +} + +.loadingOverlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.7); + border-radius: 12px; +} + +@media (max-width: 768px) { + .calendar { + padding: 12px; + } + + .cell { + min-height: 72px; + } +} diff --git a/src/apps/leave-tracker/src/lib/components/Calendar/Calendar.tsx b/src/apps/leave-tracker/src/lib/components/Calendar/Calendar.tsx new file mode 100644 index 000000000..1a803bd72 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/Calendar/Calendar.tsx @@ -0,0 +1,110 @@ +import classNames from 'classnames' +import { FC, useMemo } from 'react' + +import { LoadingSpinner } from '~/libs/ui' + +import { LeaveDate } from '../../models' +import { + getDateKey, + getMonthDates, + getStatusColor, + getStatusForDate, +} from '../../utils' + +import styles from './Calendar.module.scss' + +const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + +interface CalendarProps { + currentDate: Date + leaveDates: LeaveDate[] + selectedDates: Set + onDateClick: (dateKey: string) => void + isLoading: boolean +} + +export const Calendar: FC = ({ + currentDate, + isLoading, + leaveDates, + onDateClick, + selectedDates, +}) => { + const monthDates = useMemo( + () => getMonthDates(currentDate.getFullYear(), currentDate.getMonth()), + [currentDate], + ) + + const paddedDates = useMemo(() => { + if (!monthDates.length) { + return [] + } + + const padding = monthDates[0].getDay() + const cells: Array = [] + + for (let i = 0; i < padding; i += 1) { + cells.push(null) + } + + cells.push(...monthDates) + + while (cells.length % 7 !== 0) { + cells.push(null) + } + + return cells + }, [monthDates]) + + return ( +
+
+ {dayNames.map(day => ( +
+ {day} +
+ ))} +
+ +
+ {paddedDates.map((date, index) => { + if (!date) { + return
+ } + + const dateKey = getDateKey(date) + const status = getStatusForDate(date, leaveDates) + const isSelected = selectedDates.has(dateKey) + const statusClass = styles[getStatusColor(status)] + + return ( + + ) + })} +
+ + {isLoading && ( +
+ +
+ )} +
+ ) +} + +export default Calendar diff --git a/src/apps/leave-tracker/src/lib/components/Calendar/index.ts b/src/apps/leave-tracker/src/lib/components/Calendar/index.ts new file mode 100644 index 000000000..8c50cf89e --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/Calendar/index.ts @@ -0,0 +1 @@ +export * from './Calendar' diff --git a/src/apps/leave-tracker/src/lib/components/CalendarLegend/CalendarLegend.module.scss b/src/apps/leave-tracker/src/lib/components/CalendarLegend/CalendarLegend.module.scss new file mode 100644 index 000000000..4447f6d9d --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/CalendarLegend/CalendarLegend.module.scss @@ -0,0 +1,42 @@ +.legend { + display: flex; + flex-wrap: wrap; + gap: 12px 20px; + align-items: center; + margin: 8px 0 16px; +} + +.item { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #4b5563; +} + +.color { + width: 14px; + height: 14px; + border-radius: 4px; + border: 1px solid var(--calendar-border); +} + +.status-available { + background: var(--status-available); +} + +.status-leave { + background: var(--status-leave); +} + +.status-holiday { + background: var(--status-holiday); +} + +.status-weekend { + background: var(--status-weekend); +} + +.label { + white-space: nowrap; +} diff --git a/src/apps/leave-tracker/src/lib/components/CalendarLegend/CalendarLegend.tsx b/src/apps/leave-tracker/src/lib/components/CalendarLegend/CalendarLegend.tsx new file mode 100644 index 000000000..9c85cfc24 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/CalendarLegend/CalendarLegend.tsx @@ -0,0 +1,19 @@ +import classNames from 'classnames' +import { FC } from 'react' + +import { legendItems } from '../../utils' + +import styles from './CalendarLegend.module.scss' + +export const CalendarLegend: FC = () => ( +
+ {legendItems.map(item => ( +
+ + {item.label} +
+ ))} +
+) + +export default CalendarLegend diff --git a/src/apps/leave-tracker/src/lib/components/CalendarLegend/index.ts b/src/apps/leave-tracker/src/lib/components/CalendarLegend/index.ts new file mode 100644 index 000000000..99e195112 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/CalendarLegend/index.ts @@ -0,0 +1 @@ +export * from './CalendarLegend' diff --git a/src/apps/leave-tracker/src/lib/components/Layout/Layout.module.scss b/src/apps/leave-tracker/src/lib/components/Layout/Layout.module.scss new file mode 100644 index 000000000..75f7d6386 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/Layout/Layout.module.scss @@ -0,0 +1,55 @@ +@import '@libs/ui/styles/includes'; + +.layout { + position: relative; + font-family: $font-roboto; + color: var(--Primary); + background: #ffffff; + border: 1px solid var(--BorderColor, #e0e0e0); + border-radius: 10px; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08); + padding: $sp-5; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + gap: $sp-3; + flex-wrap: wrap; + margin-bottom: $sp-4; +} + +.headerActions { + display: flex; + align-items: center; + gap: $sp-2; + + @include ltemd { + width: 100%; + justify-content: flex-start; + } +} + +.title { + margin: 0; + font-family: $font-roboto; + font-size: 24px; + font-weight: 700; + color: var(--Primary); +} + +.main { + @include ltelg { + padding-top: $sp-3; + } +} + +.contentLayoutOuter { + margin: $sp-6 auto !important; +} + +.contentLayoutInner { + box-sizing: border-box; + width: 100%; +} diff --git a/src/apps/leave-tracker/src/lib/components/Layout/Layout.tsx b/src/apps/leave-tracker/src/lib/components/Layout/Layout.tsx new file mode 100644 index 000000000..b64282058 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/Layout/Layout.tsx @@ -0,0 +1,59 @@ +import { FC, PropsWithChildren } from 'react' +import { useLocation, useNavigate } from 'react-router-dom' + +import { Button, ContentLayout, IconOutline } from '~/libs/ui' +import { APP_NAME } from '../../../config/index.config' +import { rootRoute, teamCalendarRouteId } from '../../../config/routes.config' + +import styles from './Layout.module.scss' + +export const NullLayout: FC = props => ( + <>{props.children} +) + +export const Layout: FC = props => { + const location = useLocation() + const navigate = useNavigate() + + const buildPath = (...parts: string[]): string => { + const cleanedParts = parts + .filter(Boolean) + .map(part => part.replace(/^\/+|\/+$/g, '')) + + return `/${cleanedParts.join('/')}` || '/' + } + + const normalizedRootPath = rootRoute || '' + const teamCalendarPath = buildPath(normalizedRootPath, teamCalendarRouteId) + const personalCalendarPath = buildPath(normalizedRootPath) + const normalizedCurrentPath = location.pathname.replace(/\/+$/, '') || '/' + const isTeamCalendar = normalizedCurrentPath === teamCalendarPath + const buttonLabel = isTeamCalendar ? 'View My Calendar' : 'View Team Leave' + const buttonIcon = isTeamCalendar ? IconOutline.UserIcon : IconOutline.UsersIcon + const targetPath = isTeamCalendar ? personalCalendarPath : teamCalendarPath + + return ( + +
+
+

{APP_NAME}

+
+ +
+
+
{props.children}
+
+
+ ) +} + +export default Layout diff --git a/src/apps/leave-tracker/src/lib/components/Layout/index.ts b/src/apps/leave-tracker/src/lib/components/Layout/index.ts new file mode 100644 index 000000000..19b84975d --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/Layout/index.ts @@ -0,0 +1 @@ +export * from './Layout' diff --git a/src/apps/leave-tracker/src/lib/components/MonthNavigation/MonthNavigation.module.scss b/src/apps/leave-tracker/src/lib/components/MonthNavigation/MonthNavigation.module.scss new file mode 100644 index 000000000..488adf5d1 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/MonthNavigation/MonthNavigation.module.scss @@ -0,0 +1,32 @@ +.navigation { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin-bottom: 16px; +} + +.navButton { + min-width: 44px; + height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.currentMonth { + font-size: 20px; + font-weight: 700; + letter-spacing: 0.01em; + color: #1f2933; +} + +@media (max-width: 480px) { + .navigation { + gap: 8px; + } + + .currentMonth { + font-size: 18px; + } +} diff --git a/src/apps/leave-tracker/src/lib/components/MonthNavigation/MonthNavigation.tsx b/src/apps/leave-tracker/src/lib/components/MonthNavigation/MonthNavigation.tsx new file mode 100644 index 000000000..27f1599e5 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/MonthNavigation/MonthNavigation.tsx @@ -0,0 +1,40 @@ +import { FC } from 'react' + +import { Button, IconOutline } from '~/libs/ui' + +import { formatMonthYear } from '../../utils' + +import styles from './MonthNavigation.module.scss' + +interface MonthNavigationProps { + currentDate: Date + onNextMonth: () => void + onPrevMonth: () => void +} + +export const MonthNavigation: FC = ({ + currentDate, + onNextMonth, + onPrevMonth, +}) => ( +
+
+) + +export default MonthNavigation diff --git a/src/apps/leave-tracker/src/lib/components/MonthNavigation/index.ts b/src/apps/leave-tracker/src/lib/components/MonthNavigation/index.ts new file mode 100644 index 000000000..86e180167 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/MonthNavigation/index.ts @@ -0,0 +1 @@ +export * from './MonthNavigation' diff --git a/src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.module.scss b/src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.module.scss new file mode 100644 index 000000000..d0ff3d3b8 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.module.scss @@ -0,0 +1,151 @@ +@import '@libs/ui/styles/includes'; + +.teamCalendar { + position: relative; + background: #ffffff; + border: 1px solid var(--calendar-border); + border-radius: 12px; + padding: 16px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04); +} + +.dayNames { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 8px; + margin-bottom: 12px; + color: #6c757d; + text-align: center; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.dayName { + padding: 4px 0; +} + +.grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 10px; +} + +.cell { + position: relative; + min-height: 120px; + width: 100%; + border-radius: 10px; + border: 1px solid var(--team-cell-border, #e5e7eb); + background: #f9fafb; + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 12px; + gap: 8px; + transition: background-color 0.12s ease, transform 0.12s ease, box-shadow 0.12s ease; +} + +.cell:hover { + transform: translateY(-1px); + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.05); + background: #f3f4f6; +} + +.empty { + background: transparent; + border: none; + pointer-events: none; +} + +.weekend { + background: #f0f7ff; + border-color: #dbeafe; +} + +.dateNumber { + align-self: flex-end; + font-size: 16px; + font-weight: 800; + color: #111827; + line-height: 1; +} + +.userList { + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; +} + +.userItem { + width: 100%; + border-radius: 8px; + padding: 8px 10px; + font-weight: 700; + font-size: 14px; + line-height: 1.2; + border: 1px solid transparent; +} + +.userLeave { + background: var(--user-leave-bg, #fee2e2); + border-color: #fecdd3; + color: #9b1c1c; +} + +.userHoliday { + background: var(--user-holiday-bg, #fef3c7); + border-color: #fde68a; + color: #92400e; +} + +.emptyState { + color: #6b7280; + font-size: 13px; + font-weight: 600; +} + +.overflowIndicator { + color: #374151; + font-size: 13px; + font-weight: 700; +} + +.loading { + pointer-events: none; +} + +.loadingOverlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.7); + border-radius: 12px; +} + +@media (max-width: 768px) { + .teamCalendar { + padding: 12px; + } + + .grid { + gap: 8px; + } + + .cell { + min-height: 96px; + padding: 10px; + } + + .userItem { + font-size: 13px; + } + + .dateNumber { + font-size: 15px; + } +} diff --git a/src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.tsx b/src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.tsx new file mode 100644 index 000000000..9aa8dbae7 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.tsx @@ -0,0 +1,120 @@ +import classNames from 'classnames' +import { isWeekend } from 'date-fns' +import { FC, useMemo } from 'react' + +import { LoadingSpinner } from '~/libs/ui' + +import { LeaveStatus, TeamLeaveDate } from '../../models' +import { getDateKey, getMonthDates } from '../../utils' + +import styles from './TeamCalendar.module.scss' + +const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + +interface TeamCalendarProps { + currentDate: Date + teamLeaveDates: TeamLeaveDate[] + isLoading: boolean +} + +export const TeamCalendar: FC = ({ + currentDate, + isLoading, + teamLeaveDates, +}) => { + const monthDates = useMemo( + () => getMonthDates(currentDate.getFullYear(), currentDate.getMonth()), + [currentDate], + ) + + const paddedDates = useMemo(() => { + if (!monthDates.length) { + return [] + } + + const padding = monthDates[0].getDay() + const cells: Array = [] + + for (let i = 0; i < padding; i += 1) { + cells.push(null) + } + + cells.push(...monthDates) + + while (cells.length % 7 !== 0) { + cells.push(null) + } + + return cells + }, [monthDates]) + + return ( +
+
+ {dayNames.map(day => ( +
+ {day} +
+ ))} +
+ +
+ {paddedDates.map((date, index) => { + if (!date) { + return
+ } + + const dateKey = getDateKey(date) + const leaveEntry = teamLeaveDates.find(item => item.date === dateKey) + const users = leaveEntry?.usersOnLeave ?? [] + const displayedUsers = users.slice(0, 10) + const overflowCount = users.length - displayedUsers.length + const weekendClass = isWeekend(date) ? styles.weekend : undefined + + return ( +
+ {date.getDate()} +
+ {displayedUsers.length ? ( + displayedUsers.map((user, userIndex) => ( +
+ {user.handle ?? user.userId} +
+ )) + ) : ( +
No leave
+ )} + {overflowCount > 0 && ( +
+ +{overflowCount} more +
+ )} +
+
+ ) + })} +
+ + {isLoading && ( +
+ +
+ )} +
+ ) +} + +export default TeamCalendar diff --git a/src/apps/leave-tracker/src/lib/components/TeamCalendar/index.ts b/src/apps/leave-tracker/src/lib/components/TeamCalendar/index.ts new file mode 100644 index 000000000..1aa33e31a --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/TeamCalendar/index.ts @@ -0,0 +1 @@ +export * from './TeamCalendar' diff --git a/src/apps/leave-tracker/src/lib/components/index.ts b/src/apps/leave-tracker/src/lib/components/index.ts new file mode 100644 index 000000000..705dd2af7 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/components/index.ts @@ -0,0 +1,5 @@ +export * from './Layout' +export * from './Calendar' +export * from './MonthNavigation' +export * from './CalendarLegend' +export * from './TeamCalendar' diff --git a/src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContext.ts b/src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContext.ts new file mode 100644 index 000000000..dba938fd8 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContext.ts @@ -0,0 +1,9 @@ +import { createContext } from 'react' + +import type { LeaveTrackerContextModel } from '../models' + +export const LeaveTrackerContext = createContext({ + loginUserInfo: undefined, +}) + +export default LeaveTrackerContext diff --git a/src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContextProvider.tsx b/src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContextProvider.tsx new file mode 100644 index 000000000..ef7f287c5 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContextProvider.tsx @@ -0,0 +1,40 @@ +import { + FC, + PropsWithChildren, + useEffect, + useMemo, + useState, +} from 'react' + +import { tokenGetAsync, TokenModel } from '~/libs/core' + +import type { LeaveTrackerContextModel } from '../models' + +import { LeaveTrackerContext } from './LeaveTrackerContext' + +export const LeaveTrackerContextProvider: FC = props => { + const [loginUserInfo, setLoginUserInfo] = useState(undefined) + + const value = useMemo( + () => ({ + loginUserInfo, + }), + [loginUserInfo], + ) + + useEffect(() => { + tokenGetAsync() + .then((token: TokenModel) => setLoginUserInfo(token)) + .catch(() => { + // no-op, consumer can handle missing token + }) + }, []) + + return ( + + {props.children} + + ) +} + +export default LeaveTrackerContextProvider diff --git a/src/apps/leave-tracker/src/lib/contexts/SWRConfigProvider.tsx b/src/apps/leave-tracker/src/lib/contexts/SWRConfigProvider.tsx new file mode 100644 index 000000000..c9efac909 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/contexts/SWRConfigProvider.tsx @@ -0,0 +1,19 @@ +import { FC, PropsWithChildren } from 'react' +import { SWRConfig } from 'swr' + +import { xhrGetAsync } from '~/libs/core' + +export const SWRConfigProvider: FC = props => ( + xhrGetAsync(resource), + refreshInterval: 0, + revalidateOnFocus: false, + revalidateOnMount: true, + }} + > + {props.children} + +) + +export default SWRConfigProvider diff --git a/src/apps/leave-tracker/src/lib/contexts/index.ts b/src/apps/leave-tracker/src/lib/contexts/index.ts new file mode 100644 index 000000000..3170d57be --- /dev/null +++ b/src/apps/leave-tracker/src/lib/contexts/index.ts @@ -0,0 +1,3 @@ +export * from './LeaveTrackerContext' +export * from './LeaveTrackerContextProvider' +export * from './SWRConfigProvider' diff --git a/src/apps/leave-tracker/src/lib/hooks/index.ts b/src/apps/leave-tracker/src/lib/hooks/index.ts new file mode 100644 index 000000000..19778633f --- /dev/null +++ b/src/apps/leave-tracker/src/lib/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useFetchLeaveDates' +export * from './useFetchTeamLeave' diff --git a/src/apps/leave-tracker/src/lib/hooks/useFetchLeaveDates.ts b/src/apps/leave-tracker/src/lib/hooks/useFetchLeaveDates.ts new file mode 100644 index 000000000..5e311249d --- /dev/null +++ b/src/apps/leave-tracker/src/lib/hooks/useFetchLeaveDates.ts @@ -0,0 +1,91 @@ +import { useCallback, useRef, useState } from 'react' + +import { handleError } from '~/libs/shared' + +import { LeaveDate, LeaveUpdateStatus } from '../models' +import { fetchUserLeaveDates, setLeaveDates as setLeaveDatesService } from '../services' +import { getDateKey } from '../utils' + +export interface UseFetchLeaveDatesResult { + leaveDates: LeaveDate[] + isLoading: boolean + isUpdating: boolean + error: unknown + loadLeaveDates: (startDate?: Date, endDate?: Date) => Promise + updateLeaveDates: (dates: string[], status: LeaveUpdateStatus) => Promise +} + +const buildRequestKey = (...parts: Array): string => + parts.filter(Boolean).join('|') + +export function useFetchLeaveDates(): UseFetchLeaveDatesResult { + const [leaveDates, setLeaveDates] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [isUpdating, setIsUpdating] = useState(false) + const [error, setError] = useState(null) + const latestLoadRequestRef = useRef('') + const latestUpdateRequestRef = useRef('') + + const loadLeaveDates = useCallback( + async (startDate?: Date, endDate?: Date) => { + const startKey = startDate ? getDateKey(startDate) : undefined + const endKey = endDate ? getDateKey(endDate) : undefined + const requestKey = buildRequestKey(startKey, endKey, Date.now().toString()) + latestLoadRequestRef.current = requestKey + setIsLoading(true) + setError(null) + + try { + const response = await fetchUserLeaveDates(startKey, endKey) + if (latestLoadRequestRef.current !== requestKey) { + return + } + setLeaveDates(response) + } catch (error) { + if (latestLoadRequestRef.current === requestKey) { + setError(error) + handleError(error) + throw error + } + } finally { + if (latestLoadRequestRef.current === requestKey) { + setIsLoading(false) + } + } + }, + [], + ) + + const updateLeaveDates = useCallback( + async (dates: string[], status: LeaveUpdateStatus) => { + const requestKey = buildRequestKey(status, dates.join(','), Date.now().toString()) + latestUpdateRequestRef.current = requestKey + setIsUpdating(true) + setError(null) + + try { + await setLeaveDatesService(dates, status) + } catch (error) { + if (latestUpdateRequestRef.current === requestKey) { + setError(error) + handleError(error) + throw error + } + } finally { + if (latestUpdateRequestRef.current === requestKey) { + setIsUpdating(false) + } + } + }, + [], + ) + + return { + error, + isLoading, + isUpdating, + leaveDates, + loadLeaveDates, + updateLeaveDates, + } +} diff --git a/src/apps/leave-tracker/src/lib/hooks/useFetchTeamLeave.ts b/src/apps/leave-tracker/src/lib/hooks/useFetchTeamLeave.ts new file mode 100644 index 000000000..33f386726 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/hooks/useFetchTeamLeave.ts @@ -0,0 +1,62 @@ +import { useCallback, useRef, useState } from 'react' + +import { handleError } from '~/libs/shared' + +import { TeamLeaveDate } from '../models' +import { fetchTeamLeave } from '../services' +import { getDateKey } from '../utils' + +export interface UseFetchTeamLeaveResult { + teamLeaveDates: TeamLeaveDate[] + isLoading: boolean + error: unknown + loadTeamLeave: (startDate?: Date, endDate?: Date) => Promise +} + +const buildRequestKey = (...parts: Array): string => + parts.filter(Boolean).join('|') + +export function useFetchTeamLeave(): UseFetchTeamLeaveResult { + const [teamLeaveDates, setTeamLeaveDates] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const latestLoadRequestRef = useRef('') + + const loadTeamLeave = useCallback( + async (startDate?: Date, endDate?: Date) => { + const startKey = startDate ? getDateKey(startDate) : undefined + const endKey = endDate ? getDateKey(endDate) : undefined + const requestKey = buildRequestKey(startKey, endKey, Date.now().toString()) + latestLoadRequestRef.current = requestKey + setIsLoading(true) + setTeamLeaveDates([]) + setError(null) + + try { + const response = await fetchTeamLeave(startKey, endKey) + if (latestLoadRequestRef.current !== requestKey) { + return + } + setTeamLeaveDates(response) + } catch (error) { + if (latestLoadRequestRef.current === requestKey) { + setError(error) + handleError(error) + throw error + } + } finally { + if (latestLoadRequestRef.current === requestKey) { + setIsLoading(false) + } + } + }, + [], + ) + + return { + error, + isLoading, + loadTeamLeave, + teamLeaveDates, + } +} diff --git a/src/apps/leave-tracker/src/lib/index.ts b/src/apps/leave-tracker/src/lib/index.ts new file mode 100644 index 000000000..d5dc44d56 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/index.ts @@ -0,0 +1,6 @@ +export * from './components' +export * from './contexts' +export * from './services' +export * from './models' +export * from './hooks' +export * from './utils' diff --git a/src/apps/leave-tracker/src/lib/models/index.ts b/src/apps/leave-tracker/src/lib/models/index.ts new file mode 100644 index 000000000..7fc6312e1 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/models/index.ts @@ -0,0 +1,28 @@ +import { TokenModel } from '~/libs/core' + +export enum LeaveStatus { + AVAILABLE = 'AVAILABLE', + LEAVE = 'LEAVE', + WEEKEND = 'WEEKEND', + WIPRO_HOLIDAY = 'WIPRO_HOLIDAY', +} + +export type LeaveUpdateStatus = LeaveStatus.AVAILABLE | LeaveStatus.LEAVE + +export interface LeaveDate { + date: string + status: LeaveStatus +} + +export interface TeamLeaveDate { + date: string + usersOnLeave: Array<{ + userId: string + handle?: string + status: LeaveStatus.LEAVE | LeaveStatus.WIPRO_HOLIDAY + }> +} + +export interface LeaveTrackerContextModel { + loginUserInfo?: TokenModel +} diff --git a/src/apps/leave-tracker/src/lib/services/index.ts b/src/apps/leave-tracker/src/lib/services/index.ts new file mode 100644 index 000000000..f5ff03efd --- /dev/null +++ b/src/apps/leave-tracker/src/lib/services/index.ts @@ -0,0 +1 @@ +export * from './leave.service' diff --git a/src/apps/leave-tracker/src/lib/services/leave.service.ts b/src/apps/leave-tracker/src/lib/services/leave.service.ts new file mode 100644 index 000000000..d1af84de1 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/services/leave.service.ts @@ -0,0 +1,32 @@ +import qs from 'qs' + +import { EnvironmentConfig } from '~/config' +import { xhrGetAsync, xhrPostAsync } from '~/libs/core' + +import type { LeaveDate, LeaveUpdateStatus, TeamLeaveDate } from '../models' + +const serializeQuery = (params: Record): string => + qs.stringify(params, { addQueryPrefix: true, skipNulls: true }) + +export const fetchUserLeaveDates = async ( + startDate?: string, + endDate?: string, +): Promise => { + const queryString = serializeQuery({ startDate, endDate }) + + return xhrGetAsync(`${EnvironmentConfig.API.V6}/leave/dates${queryString}`) +} + +export const setLeaveDates = async ( + dates: string[], + status: LeaveUpdateStatus, +): Promise => xhrPostAsync(`${EnvironmentConfig.API.V6}/leave/dates`, { dates, status }) + +export const fetchTeamLeave = async ( + startDate?: string, + endDate?: string, +): Promise => { + const queryString = serializeQuery({ startDate, endDate }) + + return xhrGetAsync(`${EnvironmentConfig.API.V6}/leave/team${queryString}`) +} diff --git a/src/apps/leave-tracker/src/lib/styles/index.scss b/src/apps/leave-tracker/src/lib/styles/index.scss new file mode 100644 index 000000000..7940d6ff5 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/styles/index.scss @@ -0,0 +1,37 @@ +@import '@libs/ui/styles/includes'; + +:root { + --LeaveColor: #4caf50; + --AvailableColor: #e8f5e9; + --HolidayColor: #ffc107; + --WeekendColor: #f5f5f5; + --BorderColor: #e0e0e0; + --user-leave-bg: #fee2e2; + --user-holiday-bg: #fef3c7; + --team-cell-border: #e5e7eb; +} + +.leave-tracker-app { + --status-available: #f0f0f0; + --status-leave: #ff6b6b; + --status-holiday: #ffd93d; + --status-weekend: #a8dadc; + --status-selected: #4dabf7; + --calendar-border: #dee2e6; + --calendar-hover: #e9ecef; + + // App-specific global styles +} + +.primaryButton { + background-color: var(--LeaveColor); + color: white; + border: none; + border-radius: 4px; + padding: 8px 16px; + cursor: pointer; + + &:hover { + opacity: 0.9; + } +} diff --git a/src/apps/leave-tracker/src/lib/utils/calendar.utils.ts b/src/apps/leave-tracker/src/lib/utils/calendar.utils.ts new file mode 100644 index 000000000..104f27d70 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/utils/calendar.utils.ts @@ -0,0 +1,56 @@ +import { eachDayOfInterval, endOfMonth, format, startOfMonth } from 'date-fns' + +import { LeaveDate, LeaveStatus } from '../models' + +const statusColorMap: Record = { + [LeaveStatus.LEAVE]: 'status-leave', + [LeaveStatus.WIPRO_HOLIDAY]: 'status-holiday', + [LeaveStatus.WEEKEND]: 'status-weekend', + [LeaveStatus.AVAILABLE]: 'status-available', +} + +const statusLabelMap: Record = { + [LeaveStatus.LEAVE]: 'Leave', + [LeaveStatus.WIPRO_HOLIDAY]: 'Wipro Holiday', + [LeaveStatus.WEEKEND]: 'Weekend', + [LeaveStatus.AVAILABLE]: 'Available', +} + +export const legendStatusOrder: LeaveStatus[] = [ + LeaveStatus.AVAILABLE, + LeaveStatus.LEAVE, + LeaveStatus.WIPRO_HOLIDAY, + LeaveStatus.WEEKEND, +] + +export const getMonthDates = (year: number, month: number): Date[] => { + const monthDate = new Date(year, month, 1) + + return eachDayOfInterval({ + end: endOfMonth(monthDate), + start: startOfMonth(monthDate), + }) +} + +export const formatMonthYear = (date: Date): string => format(date, 'LLLL yyyy') + +export const getDateKey = (date: Date): string => format(date, 'yyyy-MM-dd') + +export const getStatusForDate = (date: Date, leaveDates: LeaveDate[]): LeaveStatus => { + const dateKey = getDateKey(date) + const match = leaveDates.find(item => item.date === dateKey) + + return match?.status ?? LeaveStatus.AVAILABLE +} + +export const getStatusColor = (status: LeaveStatus): string => { + return statusColorMap[status] ?? statusColorMap[LeaveStatus.AVAILABLE] +} + +export const getStatusLabel = (status: LeaveStatus): string => statusLabelMap[status] + +export const legendItems = legendStatusOrder.map(status => ({ + label: getStatusLabel(status), + status, + statusClass: getStatusColor(status), +})) diff --git a/src/apps/leave-tracker/src/lib/utils/index.ts b/src/apps/leave-tracker/src/lib/utils/index.ts new file mode 100644 index 000000000..91a1467b7 --- /dev/null +++ b/src/apps/leave-tracker/src/lib/utils/index.ts @@ -0,0 +1 @@ +export * from './calendar.utils' diff --git a/src/apps/leave-tracker/src/pages/personal-calendar/PersonalCalendarPage.module.scss b/src/apps/leave-tracker/src/pages/personal-calendar/PersonalCalendarPage.module.scss new file mode 100644 index 000000000..e4a32af5d --- /dev/null +++ b/src/apps/leave-tracker/src/pages/personal-calendar/PersonalCalendarPage.module.scss @@ -0,0 +1,77 @@ +.page { + max-width: 1080px; + margin: 0 auto; + padding: 24px 16px 48px; +} + +.header { + margin-bottom: 12px; +} + +.subtitle { + margin: 0; + font-size: 14px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #6b7280; +} + +.title { + margin: 2px 0 0; + font-size: 28px; + font-weight: 800; + color: #111827; +} + +.navigation { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + margin: 12px 0 8px; +} + +.calendarSection { + margin: 0 auto; +} + +.actions { + margin-top: 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.actionButtons { + display: inline-flex; + gap: 12px; + flex-wrap: wrap; +} + +.selectionInfo { + font-weight: 700; + color: #374151; +} + +.error { + margin-top: 12px; + padding: 12px 16px; + background: #fff5f5; + border: 1px solid #fda4af; + border-radius: 8px; + color: #b91c1c; + font-weight: 600; +} + +@media (max-width: 720px) { + .actions { + flex-direction: column; + align-items: flex-start; + } + + .selectionInfo { + order: 2; + } +} diff --git a/src/apps/leave-tracker/src/pages/personal-calendar/PersonalCalendarPage.tsx b/src/apps/leave-tracker/src/pages/personal-calendar/PersonalCalendarPage.tsx new file mode 100644 index 000000000..866027b60 --- /dev/null +++ b/src/apps/leave-tracker/src/pages/personal-calendar/PersonalCalendarPage.tsx @@ -0,0 +1,151 @@ +import { addMonths, endOfMonth, startOfMonth, subMonths } from 'date-fns' +import { FC, useCallback, useContext, useEffect, useMemo, useState } from 'react' + +import { Button } from '~/libs/ui' + +import { Calendar, CalendarLegend, MonthNavigation } from '../../lib/components' +import { LeaveStatus } from '../../lib/models' +import { LeaveTrackerContext } from '../../lib/contexts/LeaveTrackerContext' +import { useFetchLeaveDates } from '../../lib/hooks' +import styles from './PersonalCalendarPage.module.scss' + +const PersonalCalendarPage: FC = () => { + const { loginUserInfo } = useContext(LeaveTrackerContext) + const [currentDate, setCurrentDate] = useState(new Date()) + const [selectedDates, setSelectedDates] = useState>(new Set()) + const { + error, + isLoading, + isUpdating, + leaveDates, + loadLeaveDates, + updateLeaveDates, + } = useFetchLeaveDates() + const [actionError, setActionError] = useState('') + + const loadCurrentMonth = useCallback(async () => { + setActionError('') + try { + await loadLeaveDates( + startOfMonth(currentDate), + endOfMonth(currentDate), + ) + } catch { + setActionError('Unable to load leave dates. Please try again.') + } + }, [currentDate, loadLeaveDates]) + + useEffect(() => { + void loadCurrentMonth() + }, [loadCurrentMonth]) + + const handlePrevMonth = useCallback(() => { + setSelectedDates(new Set()) + setCurrentDate(prev => subMonths(prev, 1)) + }, []) + + const handleNextMonth = useCallback(() => { + setSelectedDates(new Set()) + setCurrentDate(prev => addMonths(prev, 1)) + }, []) + + const handleDateClick = useCallback((dateKey: string) => { + setSelectedDates(prev => { + const next = new Set(prev) + if (next.has(dateKey)) { + next.delete(dateKey) + } else { + next.add(dateKey) + } + return next + }) + }, []) + + const handleSetAsLeave = useCallback(async () => { + if (!selectedDates.size) return + + setActionError('') + try { + await updateLeaveDates(Array.from(selectedDates), LeaveStatus.LEAVE) + setSelectedDates(new Set()) + await loadCurrentMonth() + } catch { + setActionError('Unable to update leave dates. Please try again.') + } + }, [loadCurrentMonth, selectedDates, updateLeaveDates]) + + const handleSetAsAvailable = useCallback(async () => { + if (!selectedDates.size) return + + setActionError('') + try { + await updateLeaveDates(Array.from(selectedDates), LeaveStatus.AVAILABLE) + setSelectedDates(new Set()) + await loadCurrentMonth() + } catch { + setActionError('Unable to update leave dates. Please try again.') + } + }, [loadCurrentMonth, selectedDates, updateLeaveDates]) + + const selectionLabel = useMemo(() => { + const count = selectedDates.size + if (!count) return 'No dates selected' + return `${count} date${count > 1 ? 's' : ''} selected` + }, [selectedDates]) + + const errorMessage = actionError || (error ? 'Something went wrong. Please try again.' : '') + + return ( +
+
+

Welcome back

+

{loginUserInfo?.handle ?? 'Your calendar'}

+
+ +
+ + +
+ +
+ +
+ +
+
+ + +
+
{selectionLabel}
+
+ + {errorMessage && ( +
{errorMessage}
+ )} +
+ ) +} + +export default PersonalCalendarPage diff --git a/src/apps/leave-tracker/src/pages/personal-calendar/index.ts b/src/apps/leave-tracker/src/pages/personal-calendar/index.ts new file mode 100644 index 000000000..94d4b12da --- /dev/null +++ b/src/apps/leave-tracker/src/pages/personal-calendar/index.ts @@ -0,0 +1 @@ +export { default } from './PersonalCalendarPage' diff --git a/src/apps/leave-tracker/src/pages/team-calendar/TeamCalendarPage.module.scss b/src/apps/leave-tracker/src/pages/team-calendar/TeamCalendarPage.module.scss new file mode 100644 index 000000000..4735ddd7f --- /dev/null +++ b/src/apps/leave-tracker/src/pages/team-calendar/TeamCalendarPage.module.scss @@ -0,0 +1,46 @@ +.page { + max-width: 1200px; + margin: 0 auto; + padding: 24px 16px 48px; +} + +.header { + margin-bottom: 8px; +} + +.title { + margin: 0; + font-size: 28px; + font-weight: 800; + color: #111827; +} + +.navigation { + display: flex; + justify-content: center; + margin: 14px 0 10px; +} + +.calendarSection { + margin: 0 auto; +} + +.error { + margin-top: 12px; + padding: 12px 16px; + background: #fff5f5; + border: 1px solid #fda4af; + border-radius: 8px; + color: #b91c1c; + font-weight: 600; +} + +@media (max-width: 720px) { + .page { + padding: 20px 12px 32px; + } + + .title { + font-size: 24px; + } +} diff --git a/src/apps/leave-tracker/src/pages/team-calendar/TeamCalendarPage.tsx b/src/apps/leave-tracker/src/pages/team-calendar/TeamCalendarPage.tsx new file mode 100644 index 000000000..06895b2ea --- /dev/null +++ b/src/apps/leave-tracker/src/pages/team-calendar/TeamCalendarPage.tsx @@ -0,0 +1,73 @@ +import { addMonths, endOfMonth, startOfMonth, subMonths } from 'date-fns' +import { FC, useCallback, useEffect, useMemo, useState } from 'react' + +import { MonthNavigation, TeamCalendar } from '../../lib/components' +import { useFetchTeamLeave } from '../../lib/hooks' + +import styles from './TeamCalendarPage.module.scss' + +const TeamCalendarPage: FC = () => { + const [currentDate, setCurrentDate] = useState(new Date()) + const [actionError, setActionError] = useState('') + const { error, isLoading, loadTeamLeave, teamLeaveDates } = useFetchTeamLeave() + + const loadCurrentMonth = useCallback(async () => { + setActionError('') + try { + await loadTeamLeave( + startOfMonth(currentDate), + endOfMonth(currentDate), + ) + } catch { + setActionError('Unable to load team leave. Please try again.') + } + }, [currentDate, loadTeamLeave]) + + useEffect(() => { + void loadCurrentMonth() + }, [loadCurrentMonth]) + + const handlePrevMonth = useCallback(() => { + setCurrentDate(prev => subMonths(prev, 1)) + }, []) + + const handleNextMonth = useCallback(() => { + setCurrentDate(prev => addMonths(prev, 1)) + }, []) + + const errorMessage = useMemo(() => { + if (actionError) return actionError + if (error) return 'Something went wrong. Please try again.' + return '' + }, [actionError, error]) + + return ( +
+
+

Team Leave Calendar

+
+ +
+ +
+ +
+ +
+ + {errorMessage && ( +
{errorMessage}
+ )} +
+ ) +} + +export default TeamCalendarPage diff --git a/src/apps/leave-tracker/src/pages/team-calendar/index.ts b/src/apps/leave-tracker/src/pages/team-calendar/index.ts new file mode 100644 index 000000000..6aecf3055 --- /dev/null +++ b/src/apps/leave-tracker/src/pages/team-calendar/index.ts @@ -0,0 +1 @@ +export { default } from './TeamCalendarPage' diff --git a/src/apps/platform/src/platform.routes.tsx b/src/apps/platform/src/platform.routes.tsx index e32c8b239..89d2b78cb 100644 --- a/src/apps/platform/src/platform.routes.tsx +++ b/src/apps/platform/src/platform.routes.tsx @@ -11,6 +11,7 @@ import { walletAdminRoutes } from '~/apps/wallet-admin' import { copilotsRoutes } from '~/apps/copilots' import { adminRoutes } from '~/apps/admin' import { reviewRoutes } from '~/apps/review' +import { leaveTrackerRoutes } from '~/apps/leave-tracker' const Home: LazyLoadedComponent = lazyLoad( () => import('./routes/home'), @@ -39,6 +40,7 @@ export const platformRoutes: Array = [ ...walletAdminRoutes, ...accountsRoutes, ...reviewRoutes, + ...leaveTrackerRoutes, ...homeRoutes, ...adminRoutes, ] diff --git a/src/apps/review/src/lib/hooks/useRole.ts b/src/apps/review/src/lib/hooks/useRole.ts index 6e6bcac5a..1da964b2c 100644 --- a/src/apps/review/src/lib/hooks/useRole.ts +++ b/src/apps/review/src/lib/hooks/useRole.ts @@ -20,7 +20,7 @@ export interface useRoleProps { hasCheckpointScreenerRole: boolean hasCheckpointReviewerRole: boolean hasScreenerRole: boolean - /** Indicates the user has at least one reviewer resource assignment. */ + /** Indicates the user has at least one reviewer-eligible resource assignment. */ hasReviewerRole: boolean hasApproverRole: boolean hasPostMortemReviewerRole: boolean @@ -63,44 +63,6 @@ const useRole = (): useRoleProps => { [isTopcoderAdmin, myRoles], ) - // Get role for review flow - const actionChallengeRole = useMemo(() => { - if (!challengeId) { - return '' - } - - const normalizedRoles = [ - ...myRoles.map(role => role.toLowerCase()), - ...(isTopcoderAdmin ? ['admin'] : []), - ] - const rolePriority: ChallengeRole[] = [ - 'Admin', - 'Manager', - 'Copilot', - 'Reviewer', - 'Submitter', - ] - - const matchedRole = rolePriority.find(item => ( - normalizedRoles.some(role => role.includes(item.toLowerCase())) - )) as ChallengeRole | undefined - - if (matchedRole) { - return matchedRole - } - - if (isTopcoderAdmin) { - return 'Admin' - } - - return '' - }, [challengeId, isTopcoderAdmin, myRoles]) - - const isCopilot = useMemo( - () => actionChallengeRole === 'Copilot', - [actionChallengeRole], - ) - const checkpointScreenerResourceIds = useMemo>( () => new Set( (myResources ?? []) @@ -168,6 +130,68 @@ const useRole = (): useRoleProps => { [myResources], ) + const reviewerLikeResourceIds = useMemo( + () => { + const ids = new Set() + + checkpointReviewerResourceIds.forEach(id => ids.add(id)) + checkpointScreenerResourceIds.forEach(id => ids.add(id)) + screenerResourceIds.forEach(id => ids.add(id)) + reviewerResourceIds.forEach(id => ids.add(id)) + approverResourceIds.forEach(id => ids.add(id)) + postMortemReviewerResourceIds.forEach(id => ids.add(id)) + + return ids + }, + [ + approverResourceIds, + checkpointReviewerResourceIds, + checkpointScreenerResourceIds, + postMortemReviewerResourceIds, + reviewerResourceIds, + screenerResourceIds, + ], + ) + + // Get role for review flow + const actionChallengeRole = useMemo(() => { + if (!challengeId) { + return '' + } + + const normalizedRoles = [ + ...myRoles.map(role => role.toLowerCase()), + ...(isTopcoderAdmin ? ['admin'] : []), + ...(reviewerLikeResourceIds.size > 0 ? ['reviewer'] : []), + ] + const rolePriority: ChallengeRole[] = [ + 'Admin', + 'Manager', + 'Copilot', + 'Reviewer', + 'Submitter', + ] + + const matchedRole = rolePriority.find(item => ( + normalizedRoles.some(role => role.includes(item.toLowerCase())) + )) as ChallengeRole | undefined + + if (matchedRole) { + return matchedRole + } + + if (isTopcoderAdmin) { + return 'Admin' + } + + return '' + }, [challengeId, isTopcoderAdmin, myRoles, reviewerLikeResourceIds.size]) + + const isCopilot = useMemo( + () => actionChallengeRole === 'Copilot', + [actionChallengeRole], + ) + const copilotReviewerResourceIds = useMemo>( () => { if (!isCopilot) { @@ -202,8 +226,8 @@ const useRole = (): useRoleProps => { ) const hasReviewerRole = useMemo( - () => reviewerResourceIds.size > 0, - [reviewerResourceIds], + () => reviewerLikeResourceIds.size > 0, + [reviewerLikeResourceIds], ) const hasApproverRole = useMemo( diff --git a/src/config/constants.ts b/src/config/constants.ts index cdcd34df1..1f7ab2a90 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -12,6 +12,7 @@ export enum AppSubdomain { copilots = 'copilots', admin = 'system-admin', review = 'review', + leaveTracker = 'leave-tracker', } export enum ToolTitle { @@ -28,6 +29,7 @@ export enum ToolTitle { copilots = 'Copilots', admin = 'Admin', review = 'Review', + leaveTracker = 'Leave Tracker', } export const PageSubheaderPortalId: string = 'page-subheader-portal-el' diff --git a/src/config/environments/local.env.ts b/src/config/environments/local.env.ts index 38feb2218..ab3f635ab 100644 --- a/src/config/environments/local.env.ts +++ b/src/config/environments/local.env.ts @@ -38,6 +38,8 @@ export const LOCAL_SERVICE_OVERRIDES: LocalServiceOverride[] = [ { prefix: '/v6/resources', target: 'http://localhost:3004' }, { prefix: '/v6/resource-roles', target: 'http://localhost:3004' }, + { prefix: '/v6/leave', target: 'http://localhost:3011' }, + { prefix: '/v6/reviewSummations', target: 'http://localhost:3005' }, { prefix: '/v6/reviewTypes', target: 'http://localhost:3005' }, { prefix: '/v6/reviews', target: 'http://localhost:3005' }, diff --git a/src/libs/core/lib/profile/profile-functions/profile-factory/user-role.enum.ts b/src/libs/core/lib/profile/profile-functions/profile-factory/user-role.enum.ts index 321da1828..612cbde4c 100644 --- a/src/libs/core/lib/profile/profile-functions/profile-factory/user-role.enum.ts +++ b/src/libs/core/lib/profile/profile-functions/profile-factory/user-role.enum.ts @@ -12,5 +12,6 @@ export enum UserRole { projectManager = 'Project Manager', taxFormAdmin = 'TaxForm Admin', taxFormViewer = 'TaxForm Viewer', - copilot = 'copilot' + copilot = 'copilot', + topcoderStaff = 'Topcoder Staff' } From 8ab54f78c1a42c210d5360967cfe796bffde7091 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 6 Jan 2026 09:56:31 +1100 Subject: [PATCH 02/11] Add Slack and email reminder functionality (needs testing / Slack admin access) --- src/apps/{leave-tracker => calendar}/index.ts | 0 .../src/CalendarApp.tsx} | 16 +- .../src/calendar-app.routes.tsx} | 24 +-- src/apps/calendar/src/config/index.config.ts | 4 + .../src/config/routes.config.ts | 4 +- src/apps/calendar/src/index.ts | 2 + .../components/Calendar/Calendar.module.scss | 0 .../src/lib/components/Calendar/Calendar.tsx | 40 +++-- .../src/lib/components/Calendar/index.ts | 0 .../CalendarLegend/CalendarLegend.module.scss | 0 .../CalendarLegend/CalendarLegend.tsx | 2 +- .../lib/components/CalendarLegend/index.ts | 0 .../lib/components/Layout/Layout.module.scss | 10 +- .../src/lib/components/Layout/Layout.tsx | 8 +- .../src/lib/components/Layout/index.ts | 0 .../MonthNavigation.module.scss | 0 .../MonthNavigation/MonthNavigation.tsx | 12 +- .../lib/components/MonthNavigation/index.ts | 0 .../TeamCalendar/TeamCalendar.module.scss | 0 .../components/TeamCalendar/TeamCalendar.tsx | 153 ++++++++++++++++++ .../src/lib/components/TeamCalendar/index.ts | 0 .../src/lib/components/index.ts | 0 .../src/lib/contexts/CalendarContext.ts | 9 ++ .../lib/contexts/CalendarContextProvider.tsx} | 14 +- .../src/lib/contexts/SWRConfigProvider.tsx | 0 src/apps/calendar/src/lib/contexts/index.ts | 3 + .../src/lib/hooks/index.ts | 0 .../src/lib/hooks/useFetchLeaveDates.ts | 44 +++-- .../src/lib/hooks/useFetchTeamLeave.ts | 27 ++-- .../src/lib/index.ts | 0 .../src/lib/models/index.ts | 4 +- .../src/lib/services/index.ts | 0 .../src/lib/services/leave.service.ts | 10 +- .../src/lib/styles/index.scss | 2 +- .../src/lib/utils/calendar.utils.ts | 6 +- .../src/lib/utils/index.ts | 0 .../PersonalCalendarPage.module.scss | 0 .../PersonalCalendarPage.tsx | 29 ++-- .../src/pages/personal-calendar/index.ts | 1 + .../TeamCalendarPage.module.scss | 0 .../pages/team-calendar/TeamCalendarPage.tsx | 8 +- .../calendar/src/pages/team-calendar/index.ts | 1 + .../leave-tracker/src/config/index.config.ts | 4 - src/apps/leave-tracker/src/index.ts | 2 - .../components/TeamCalendar/TeamCalendar.tsx | 120 -------------- .../src/lib/contexts/LeaveTrackerContext.ts | 9 -- .../leave-tracker/src/lib/contexts/index.ts | 3 - .../src/pages/personal-calendar/index.ts | 1 - .../src/pages/team-calendar/index.ts | 1 - src/apps/platform/src/platform.routes.tsx | 4 +- src/config/constants.ts | 4 +- 51 files changed, 328 insertions(+), 253 deletions(-) rename src/apps/{leave-tracker => calendar}/index.ts (100%) rename src/apps/{leave-tracker/src/LeaveTrackerApp.tsx => calendar/src/CalendarApp.tsx} (62%) rename src/apps/{leave-tracker/src/leave-tracker-app.routes.tsx => calendar/src/calendar-app.routes.tsx} (71%) create mode 100644 src/apps/calendar/src/config/index.config.ts rename src/apps/{leave-tracker => calendar}/src/config/routes.config.ts (66%) create mode 100644 src/apps/calendar/src/index.ts rename src/apps/{leave-tracker => calendar}/src/lib/components/Calendar/Calendar.module.scss (100%) rename src/apps/{leave-tracker => calendar}/src/lib/components/Calendar/Calendar.tsx (72%) rename src/apps/{leave-tracker => calendar}/src/lib/components/Calendar/index.ts (100%) rename src/apps/{leave-tracker => calendar}/src/lib/components/CalendarLegend/CalendarLegend.module.scss (100%) rename src/apps/{leave-tracker => calendar}/src/lib/components/CalendarLegend/CalendarLegend.tsx (100%) rename src/apps/{leave-tracker => calendar}/src/lib/components/CalendarLegend/index.ts (100%) rename src/apps/{leave-tracker => calendar}/src/lib/components/Layout/Layout.module.scss (82%) rename src/apps/{leave-tracker => calendar}/src/lib/components/Layout/Layout.tsx (91%) rename src/apps/{leave-tracker => calendar}/src/lib/components/Layout/index.ts (100%) rename src/apps/{leave-tracker => calendar}/src/lib/components/MonthNavigation/MonthNavigation.module.scss (100%) rename src/apps/{leave-tracker => calendar}/src/lib/components/MonthNavigation/MonthNavigation.tsx (73%) rename src/apps/{leave-tracker => calendar}/src/lib/components/MonthNavigation/index.ts (100%) rename src/apps/{leave-tracker => calendar}/src/lib/components/TeamCalendar/TeamCalendar.module.scss (100%) create mode 100644 src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx rename src/apps/{leave-tracker => calendar}/src/lib/components/TeamCalendar/index.ts (100%) rename src/apps/{leave-tracker => calendar}/src/lib/components/index.ts (100%) create mode 100644 src/apps/calendar/src/lib/contexts/CalendarContext.ts rename src/apps/{leave-tracker/src/lib/contexts/LeaveTrackerContextProvider.tsx => calendar/src/lib/contexts/CalendarContextProvider.tsx} (61%) rename src/apps/{leave-tracker => calendar}/src/lib/contexts/SWRConfigProvider.tsx (100%) create mode 100644 src/apps/calendar/src/lib/contexts/index.ts rename src/apps/{leave-tracker => calendar}/src/lib/hooks/index.ts (100%) rename src/apps/{leave-tracker => calendar}/src/lib/hooks/useFetchLeaveDates.ts (76%) rename src/apps/{leave-tracker => calendar}/src/lib/hooks/useFetchTeamLeave.ts (78%) rename src/apps/{leave-tracker => calendar}/src/lib/index.ts (100%) rename src/apps/{leave-tracker => calendar}/src/lib/models/index.ts (85%) rename src/apps/{leave-tracker => calendar}/src/lib/services/index.ts (100%) rename src/apps/{leave-tracker => calendar}/src/lib/services/leave.service.ts (80%) rename src/apps/{leave-tracker => calendar}/src/lib/styles/index.scss (97%) rename src/apps/{leave-tracker => calendar}/src/lib/utils/calendar.utils.ts (92%) rename src/apps/{leave-tracker => calendar}/src/lib/utils/index.ts (100%) rename src/apps/{leave-tracker => calendar}/src/pages/personal-calendar/PersonalCalendarPage.module.scss (100%) rename src/apps/{leave-tracker => calendar}/src/pages/personal-calendar/PersonalCalendarPage.tsx (87%) create mode 100644 src/apps/calendar/src/pages/personal-calendar/index.ts rename src/apps/{leave-tracker => calendar}/src/pages/team-calendar/TeamCalendarPage.module.scss (100%) rename src/apps/{leave-tracker => calendar}/src/pages/team-calendar/TeamCalendarPage.tsx (89%) create mode 100644 src/apps/calendar/src/pages/team-calendar/index.ts delete mode 100644 src/apps/leave-tracker/src/config/index.config.ts delete mode 100644 src/apps/leave-tracker/src/index.ts delete mode 100644 src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.tsx delete mode 100644 src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContext.ts delete mode 100644 src/apps/leave-tracker/src/lib/contexts/index.ts delete mode 100644 src/apps/leave-tracker/src/pages/personal-calendar/index.ts delete mode 100644 src/apps/leave-tracker/src/pages/team-calendar/index.ts diff --git a/src/apps/leave-tracker/index.ts b/src/apps/calendar/index.ts similarity index 100% rename from src/apps/leave-tracker/index.ts rename to src/apps/calendar/index.ts diff --git a/src/apps/leave-tracker/src/LeaveTrackerApp.tsx b/src/apps/calendar/src/CalendarApp.tsx similarity index 62% rename from src/apps/leave-tracker/src/LeaveTrackerApp.tsx rename to src/apps/calendar/src/CalendarApp.tsx index a701c9819..39bc823bc 100644 --- a/src/apps/leave-tracker/src/LeaveTrackerApp.tsx +++ b/src/apps/calendar/src/CalendarApp.tsx @@ -3,31 +3,31 @@ import { Outlet, Routes } from 'react-router-dom' import { routerContext, RouterContextData } from '~/libs/core' -import { Layout, LeaveTrackerContextProvider, SWRConfigProvider } from './lib' -import { toolTitle } from './leave-tracker-app.routes' +import { CalendarContextProvider, Layout, SWRConfigProvider } from './lib' +import { toolTitle } from './calendar-app.routes' import './lib/styles/index.scss' -const LeaveTrackerApp: FC = () => { +const CalendarApp: FC = () => { const { getChildRoutes }: RouterContextData = useContext(routerContext) const childRoutes = useMemo(() => getChildRoutes(toolTitle), [getChildRoutes]) useEffect(() => { - document.body.classList.add('leave-tracker-app') + document.body.classList.add('calendar-app') return () => { - document.body.classList.remove('leave-tracker-app') + document.body.classList.remove('calendar-app') } }, []) return ( - + {childRoutes} - + ) } -export default LeaveTrackerApp +export default CalendarApp diff --git a/src/apps/leave-tracker/src/leave-tracker-app.routes.tsx b/src/apps/calendar/src/calendar-app.routes.tsx similarity index 71% rename from src/apps/leave-tracker/src/leave-tracker-app.routes.tsx rename to src/apps/calendar/src/calendar-app.routes.tsx index 842667b5e..b3eec295f 100644 --- a/src/apps/leave-tracker/src/leave-tracker-app.routes.tsx +++ b/src/apps/calendar/src/calendar-app.routes.tsx @@ -4,23 +4,21 @@ import { UserRole } from '~/libs/core/lib/profile/profile-functions/profile-fact import { personalCalendarRouteId, rootRoute, teamCalendarRouteId } from './config/routes.config' -const LeaveTrackerApp: LazyLoadedComponent = lazyLoad(() => import('./LeaveTrackerApp')) +const CalendarApp: LazyLoadedComponent = lazyLoad(() => import('./CalendarApp')) const PersonalCalendarPage: LazyLoadedComponent = lazyLoad( () => import('./pages/personal-calendar'), + 'PersonalCalendarPage', +) +const TeamCalendarPage: LazyLoadedComponent = lazyLoad( + () => import('./pages/team-calendar'), + 'TeamCalendarPage', ) -const TeamCalendarPage: LazyLoadedComponent = lazyLoad(() => import('./pages/team-calendar')) -export const toolTitle: string = ToolTitle.leaveTracker +export const toolTitle: string = ToolTitle.calendar -export const leaveTrackerRoutes: ReadonlyArray = [ +export const calendarRoutes: ReadonlyArray = [ { - domain: AppSubdomain.leaveTracker, - element: , - id: toolTitle, - route: rootRoute, - title: toolTitle, authRequired: true, - rolesRequired: [UserRole.topcoderStaff, UserRole.administrator], children: [ { element: , @@ -35,5 +33,11 @@ export const leaveTrackerRoutes: ReadonlyArray = [ title: 'Team Calendar', }, ], + domain: AppSubdomain.calendar, + element: , + id: toolTitle, + rolesRequired: [UserRole.topcoderStaff, UserRole.administrator], + route: rootRoute, + title: toolTitle, }, ] diff --git a/src/apps/calendar/src/config/index.config.ts b/src/apps/calendar/src/config/index.config.ts new file mode 100644 index 000000000..f1a930826 --- /dev/null +++ b/src/apps/calendar/src/config/index.config.ts @@ -0,0 +1,4 @@ +/** + * Common calendar config constants. + */ +export const APP_NAME = 'Calendar' diff --git a/src/apps/leave-tracker/src/config/routes.config.ts b/src/apps/calendar/src/config/routes.config.ts similarity index 66% rename from src/apps/leave-tracker/src/config/routes.config.ts rename to src/apps/calendar/src/config/routes.config.ts index 148f14917..194a42193 100644 --- a/src/apps/leave-tracker/src/config/routes.config.ts +++ b/src/apps/calendar/src/config/routes.config.ts @@ -1,9 +1,9 @@ import { AppSubdomain, EnvironmentConfig } from '~/config' export const rootRoute: string - = EnvironmentConfig.SUBDOMAIN === AppSubdomain.leaveTracker + = EnvironmentConfig.SUBDOMAIN === AppSubdomain.calendar ? '' - : `/${AppSubdomain.leaveTracker}` + : `/${AppSubdomain.calendar}` export const personalCalendarRouteId = 'personal-calendar' export const teamCalendarRouteId = 'team-calendar' diff --git a/src/apps/calendar/src/index.ts b/src/apps/calendar/src/index.ts new file mode 100644 index 000000000..393b0b665 --- /dev/null +++ b/src/apps/calendar/src/index.ts @@ -0,0 +1,2 @@ +export { calendarRoutes } from './calendar-app.routes' +export { rootRoute as calendarRootRoute } from './config/routes.config' diff --git a/src/apps/leave-tracker/src/lib/components/Calendar/Calendar.module.scss b/src/apps/calendar/src/lib/components/Calendar/Calendar.module.scss similarity index 100% rename from src/apps/leave-tracker/src/lib/components/Calendar/Calendar.module.scss rename to src/apps/calendar/src/lib/components/Calendar/Calendar.module.scss diff --git a/src/apps/leave-tracker/src/lib/components/Calendar/Calendar.tsx b/src/apps/calendar/src/lib/components/Calendar/Calendar.tsx similarity index 72% rename from src/apps/leave-tracker/src/lib/components/Calendar/Calendar.tsx rename to src/apps/calendar/src/lib/components/Calendar/Calendar.tsx index 1a803bd72..6b2b9ff10 100644 --- a/src/apps/leave-tracker/src/lib/components/Calendar/Calendar.tsx +++ b/src/apps/calendar/src/lib/components/Calendar/Calendar.tsx @@ -1,5 +1,5 @@ +import { MouseEvent, useMemo } from 'react' import classNames from 'classnames' -import { FC, useMemo } from 'react' import { LoadingSpinner } from '~/libs/ui' @@ -23,13 +23,13 @@ interface CalendarProps { isLoading: boolean } -export const Calendar: FC = ({ - currentDate, - isLoading, - leaveDates, - onDateClick, - selectedDates, -}) => { +export const Calendar = (props: CalendarProps): JSX.Element => { + const currentDate = props.currentDate + const isLoading = props.isLoading + const leaveDates = props.leaveDates + const onDateClick = props.onDateClick + const selectedDates = props.selectedDates + const monthDates = useMemo( () => getMonthDates(currentDate.getFullYear(), currentDate.getMonth()), [currentDate], @@ -41,21 +41,29 @@ export const Calendar: FC = ({ } const padding = monthDates[0].getDay() - const cells: Array = [] + const cells: Array = [] for (let i = 0; i < padding; i += 1) { - cells.push(null) + cells.push(undefined) } cells.push(...monthDates) while (cells.length % 7 !== 0) { - cells.push(null) + cells.push(undefined) } return cells }, [monthDates]) + function handleDateClick(event: MouseEvent): void { + const dateKey = event.currentTarget.dataset.dateKey + + if (dateKey) { + onDateClick(dateKey) + } + } + return (
@@ -69,7 +77,12 @@ export const Calendar: FC = ({
{paddedDates.map((date, index) => { if (!date) { - return
+ return ( +
+ ) } const dateKey = getDateKey(date) @@ -89,7 +102,8 @@ export const Calendar: FC = ({ [styles.loading]: isLoading, }, )} - onClick={() => onDateClick(dateKey)} + data-date-key={dateKey} + onClick={handleDateClick} disabled={isLoading} > {date.getDate()} diff --git a/src/apps/leave-tracker/src/lib/components/Calendar/index.ts b/src/apps/calendar/src/lib/components/Calendar/index.ts similarity index 100% rename from src/apps/leave-tracker/src/lib/components/Calendar/index.ts rename to src/apps/calendar/src/lib/components/Calendar/index.ts diff --git a/src/apps/leave-tracker/src/lib/components/CalendarLegend/CalendarLegend.module.scss b/src/apps/calendar/src/lib/components/CalendarLegend/CalendarLegend.module.scss similarity index 100% rename from src/apps/leave-tracker/src/lib/components/CalendarLegend/CalendarLegend.module.scss rename to src/apps/calendar/src/lib/components/CalendarLegend/CalendarLegend.module.scss diff --git a/src/apps/leave-tracker/src/lib/components/CalendarLegend/CalendarLegend.tsx b/src/apps/calendar/src/lib/components/CalendarLegend/CalendarLegend.tsx similarity index 100% rename from src/apps/leave-tracker/src/lib/components/CalendarLegend/CalendarLegend.tsx rename to src/apps/calendar/src/lib/components/CalendarLegend/CalendarLegend.tsx index 9c85cfc24..375b0a04f 100644 --- a/src/apps/leave-tracker/src/lib/components/CalendarLegend/CalendarLegend.tsx +++ b/src/apps/calendar/src/lib/components/CalendarLegend/CalendarLegend.tsx @@ -1,5 +1,5 @@ -import classNames from 'classnames' import { FC } from 'react' +import classNames from 'classnames' import { legendItems } from '../../utils' diff --git a/src/apps/leave-tracker/src/lib/components/CalendarLegend/index.ts b/src/apps/calendar/src/lib/components/CalendarLegend/index.ts similarity index 100% rename from src/apps/leave-tracker/src/lib/components/CalendarLegend/index.ts rename to src/apps/calendar/src/lib/components/CalendarLegend/index.ts diff --git a/src/apps/leave-tracker/src/lib/components/Layout/Layout.module.scss b/src/apps/calendar/src/lib/components/Layout/Layout.module.scss similarity index 82% rename from src/apps/leave-tracker/src/lib/components/Layout/Layout.module.scss rename to src/apps/calendar/src/lib/components/Layout/Layout.module.scss index 75f7d6386..25e66ab2f 100644 --- a/src/apps/leave-tracker/src/lib/components/Layout/Layout.module.scss +++ b/src/apps/calendar/src/lib/components/Layout/Layout.module.scss @@ -14,7 +14,7 @@ .header { display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-end; gap: $sp-3; flex-wrap: wrap; margin-bottom: $sp-4; @@ -31,14 +31,6 @@ } } -.title { - margin: 0; - font-family: $font-roboto; - font-size: 24px; - font-weight: 700; - color: var(--Primary); -} - .main { @include ltelg { padding-top: $sp-3; diff --git a/src/apps/leave-tracker/src/lib/components/Layout/Layout.tsx b/src/apps/calendar/src/lib/components/Layout/Layout.tsx similarity index 91% rename from src/apps/leave-tracker/src/lib/components/Layout/Layout.tsx rename to src/apps/calendar/src/lib/components/Layout/Layout.tsx index b64282058..9e3a87b79 100644 --- a/src/apps/leave-tracker/src/lib/components/Layout/Layout.tsx +++ b/src/apps/calendar/src/lib/components/Layout/Layout.tsx @@ -2,7 +2,7 @@ import { FC, PropsWithChildren } from 'react' import { useLocation, useNavigate } from 'react-router-dom' import { Button, ContentLayout, IconOutline } from '~/libs/ui' -import { APP_NAME } from '../../../config/index.config' + import { rootRoute, teamCalendarRouteId } from '../../../config/routes.config' import styles from './Layout.module.scss' @@ -31,6 +31,9 @@ export const Layout: FC = props => { const buttonLabel = isTeamCalendar ? 'View My Calendar' : 'View Team Leave' const buttonIcon = isTeamCalendar ? IconOutline.UserIcon : IconOutline.UsersIcon const targetPath = isTeamCalendar ? personalCalendarPath : teamCalendarPath + function handleToggleCalendar(): void { + navigate(targetPath) + } return ( = props => { >
-

{APP_NAME}

diff --git a/src/apps/leave-tracker/src/lib/components/Layout/index.ts b/src/apps/calendar/src/lib/components/Layout/index.ts similarity index 100% rename from src/apps/leave-tracker/src/lib/components/Layout/index.ts rename to src/apps/calendar/src/lib/components/Layout/index.ts diff --git a/src/apps/leave-tracker/src/lib/components/MonthNavigation/MonthNavigation.module.scss b/src/apps/calendar/src/lib/components/MonthNavigation/MonthNavigation.module.scss similarity index 100% rename from src/apps/leave-tracker/src/lib/components/MonthNavigation/MonthNavigation.module.scss rename to src/apps/calendar/src/lib/components/MonthNavigation/MonthNavigation.module.scss diff --git a/src/apps/leave-tracker/src/lib/components/MonthNavigation/MonthNavigation.tsx b/src/apps/calendar/src/lib/components/MonthNavigation/MonthNavigation.tsx similarity index 73% rename from src/apps/leave-tracker/src/lib/components/MonthNavigation/MonthNavigation.tsx rename to src/apps/calendar/src/lib/components/MonthNavigation/MonthNavigation.tsx index 27f1599e5..edf5c2d96 100644 --- a/src/apps/leave-tracker/src/lib/components/MonthNavigation/MonthNavigation.tsx +++ b/src/apps/calendar/src/lib/components/MonthNavigation/MonthNavigation.tsx @@ -12,26 +12,22 @@ interface MonthNavigationProps { onPrevMonth: () => void } -export const MonthNavigation: FC = ({ - currentDate, - onNextMonth, - onPrevMonth, -}) => ( +export const MonthNavigation: FC = (props: MonthNavigationProps) => (
diff --git a/src/apps/leave-tracker/src/lib/components/MonthNavigation/index.ts b/src/apps/calendar/src/lib/components/MonthNavigation/index.ts similarity index 100% rename from src/apps/leave-tracker/src/lib/components/MonthNavigation/index.ts rename to src/apps/calendar/src/lib/components/MonthNavigation/index.ts diff --git a/src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.module.scss b/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.module.scss similarity index 100% rename from src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.module.scss rename to src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.module.scss diff --git a/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx b/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx new file mode 100644 index 000000000..d5cecce4e --- /dev/null +++ b/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx @@ -0,0 +1,153 @@ +import { isWeekend } from 'date-fns' +import { FC, useMemo } from 'react' +import classNames from 'classnames' + +import { LoadingSpinner } from '~/libs/ui' + +import { LeaveStatus, TeamLeaveDate } from '../../models' +import { getDateKey, getMonthDates } from '../../utils' + +import styles from './TeamCalendar.module.scss' + +const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] +type TeamLeaveUser = TeamLeaveDate['usersOnLeave'][number] + +const getUserDisplayName = (user: TeamLeaveUser): string => { + const firstName = user.firstName?.trim() + const lastName = user.lastName?.trim() + const fullName = [firstName, lastName].filter(Boolean).join(' ') + + return fullName || user.handle || user.userId +} + +const compareUsersByName = (userA: TeamLeaveUser, userB: TeamLeaveUser): number => { + const firstNameA = userA.firstName?.trim() || getUserDisplayName(userA) + const firstNameB = userB.firstName?.trim() || getUserDisplayName(userB) + const firstNameCompare = firstNameA.localeCompare(firstNameB, undefined, { sensitivity: 'base' }) + + if (firstNameCompare !== 0) { + return firstNameCompare + } + + const lastNameA = userA.lastName?.trim() ?? '' + const lastNameB = userB.lastName?.trim() ?? '' + const lastNameCompare = lastNameA.localeCompare(lastNameB, undefined, { sensitivity: 'base' }) + + if (lastNameCompare !== 0) { + return lastNameCompare + } + + return (userA.userId ?? '').localeCompare(userB.userId ?? '', undefined, { sensitivity: 'base' }) +} + +interface TeamCalendarProps { + currentDate: Date + teamLeaveDates: TeamLeaveDate[] + isLoading: boolean +} + +export const TeamCalendar: FC = (props: TeamCalendarProps) => { + const currentDate = props.currentDate + const isLoading = props.isLoading + const teamLeaveDates = props.teamLeaveDates + + const monthDates = useMemo( + () => getMonthDates(currentDate.getFullYear(), currentDate.getMonth()), + [currentDate], + ) + + const paddedDates = useMemo(() => { + if (!monthDates.length) { + return [] + } + + const padding = monthDates[0].getDay() + const cells: Array = [] + + for (let i = 0; i < padding; i += 1) { + cells.push(undefined) + } + + cells.push(...monthDates) + + while (cells.length % 7 !== 0) { + cells.push(undefined) + } + + return cells + }, [monthDates]) + + return ( +
+
+ {dayNames.map(day => ( +
+ {day} +
+ ))} +
+ +
+ {paddedDates.map((date, index) => { + if (!date) { + return ( +
+ ) + } + + const dateKey = getDateKey(date) + const leaveEntry = teamLeaveDates.find(item => item.date === dateKey) + const users = leaveEntry?.usersOnLeave ?? [] + const sortedUsers = [...users].sort(compareUsersByName) + const displayedUsers = sortedUsers.slice(0, 10) + const overflowCount = sortedUsers.length - displayedUsers.length + const weekendClass = isWeekend(date) ? styles.weekend : undefined + + return ( +
+ {date.getDate()} +
+ {displayedUsers.length + ? displayedUsers.map((user, userIndex) => ( +
+ {getUserDisplayName(user)} +
+ )) + : null} + {overflowCount > 0 && ( +
+ {`+${overflowCount} more`} +
+ )} +
+
+ ) + })} +
+ + {isLoading && ( +
+ +
+ )} +
+ ) +} + +export default TeamCalendar diff --git a/src/apps/leave-tracker/src/lib/components/TeamCalendar/index.ts b/src/apps/calendar/src/lib/components/TeamCalendar/index.ts similarity index 100% rename from src/apps/leave-tracker/src/lib/components/TeamCalendar/index.ts rename to src/apps/calendar/src/lib/components/TeamCalendar/index.ts diff --git a/src/apps/leave-tracker/src/lib/components/index.ts b/src/apps/calendar/src/lib/components/index.ts similarity index 100% rename from src/apps/leave-tracker/src/lib/components/index.ts rename to src/apps/calendar/src/lib/components/index.ts diff --git a/src/apps/calendar/src/lib/contexts/CalendarContext.ts b/src/apps/calendar/src/lib/contexts/CalendarContext.ts new file mode 100644 index 000000000..d9611437b --- /dev/null +++ b/src/apps/calendar/src/lib/contexts/CalendarContext.ts @@ -0,0 +1,9 @@ +import { createContext } from 'react' + +import type { CalendarContextModel } from '../models' + +export const CalendarContext = createContext({ + loginUserInfo: undefined, +}) + +export default CalendarContext diff --git a/src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContextProvider.tsx b/src/apps/calendar/src/lib/contexts/CalendarContextProvider.tsx similarity index 61% rename from src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContextProvider.tsx rename to src/apps/calendar/src/lib/contexts/CalendarContextProvider.tsx index ef7f287c5..550987623 100644 --- a/src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContextProvider.tsx +++ b/src/apps/calendar/src/lib/contexts/CalendarContextProvider.tsx @@ -8,14 +8,14 @@ import { import { tokenGetAsync, TokenModel } from '~/libs/core' -import type { LeaveTrackerContextModel } from '../models' +import type { CalendarContextModel } from '../models' -import { LeaveTrackerContext } from './LeaveTrackerContext' +import { CalendarContext } from './CalendarContext' -export const LeaveTrackerContextProvider: FC = props => { +export const CalendarContextProvider: FC = props => { const [loginUserInfo, setLoginUserInfo] = useState(undefined) - const value = useMemo( + const value = useMemo( () => ({ loginUserInfo, }), @@ -31,10 +31,10 @@ export const LeaveTrackerContextProvider: FC = props => { }, []) return ( - + {props.children} - + ) } -export default LeaveTrackerContextProvider +export default CalendarContextProvider diff --git a/src/apps/leave-tracker/src/lib/contexts/SWRConfigProvider.tsx b/src/apps/calendar/src/lib/contexts/SWRConfigProvider.tsx similarity index 100% rename from src/apps/leave-tracker/src/lib/contexts/SWRConfigProvider.tsx rename to src/apps/calendar/src/lib/contexts/SWRConfigProvider.tsx diff --git a/src/apps/calendar/src/lib/contexts/index.ts b/src/apps/calendar/src/lib/contexts/index.ts new file mode 100644 index 000000000..b2bdb3318 --- /dev/null +++ b/src/apps/calendar/src/lib/contexts/index.ts @@ -0,0 +1,3 @@ +export * from './CalendarContext' +export * from './CalendarContextProvider' +export * from './SWRConfigProvider' diff --git a/src/apps/leave-tracker/src/lib/hooks/index.ts b/src/apps/calendar/src/lib/hooks/index.ts similarity index 100% rename from src/apps/leave-tracker/src/lib/hooks/index.ts rename to src/apps/calendar/src/lib/hooks/index.ts diff --git a/src/apps/leave-tracker/src/lib/hooks/useFetchLeaveDates.ts b/src/apps/calendar/src/lib/hooks/useFetchLeaveDates.ts similarity index 76% rename from src/apps/leave-tracker/src/lib/hooks/useFetchLeaveDates.ts rename to src/apps/calendar/src/lib/hooks/useFetchLeaveDates.ts index 5e311249d..7a445f68d 100644 --- a/src/apps/leave-tracker/src/lib/hooks/useFetchLeaveDates.ts +++ b/src/apps/calendar/src/lib/hooks/useFetchLeaveDates.ts @@ -15,14 +15,17 @@ export interface UseFetchLeaveDatesResult { updateLeaveDates: (dates: string[], status: LeaveUpdateStatus) => Promise } -const buildRequestKey = (...parts: Array): string => - parts.filter(Boolean).join('|') +const buildRequestKey = (...parts: Array): string => ( + parts + .filter(Boolean) + .join('|') +) export function useFetchLeaveDates(): UseFetchLeaveDatesResult { const [leaveDates, setLeaveDates] = useState([]) const [isLoading, setIsLoading] = useState(false) const [isUpdating, setIsUpdating] = useState(false) - const [error, setError] = useState(null) + const [error, setError] = useState(undefined) const latestLoadRequestRef = useRef('') const latestUpdateRequestRef = useRef('') @@ -30,22 +33,28 @@ export function useFetchLeaveDates(): UseFetchLeaveDatesResult { async (startDate?: Date, endDate?: Date) => { const startKey = startDate ? getDateKey(startDate) : undefined const endKey = endDate ? getDateKey(endDate) : undefined - const requestKey = buildRequestKey(startKey, endKey, Date.now().toString()) + const requestKey = buildRequestKey( + startKey, + endKey, + Date.now() + .toString(), + ) latestLoadRequestRef.current = requestKey setIsLoading(true) - setError(null) + setError(undefined) try { const response = await fetchUserLeaveDates(startKey, endKey) if (latestLoadRequestRef.current !== requestKey) { return } + setLeaveDates(response) - } catch (error) { + } catch (err) { if (latestLoadRequestRef.current === requestKey) { - setError(error) - handleError(error) - throw error + setError(err) + handleError(err) + throw err } } finally { if (latestLoadRequestRef.current === requestKey) { @@ -58,18 +67,23 @@ export function useFetchLeaveDates(): UseFetchLeaveDatesResult { const updateLeaveDates = useCallback( async (dates: string[], status: LeaveUpdateStatus) => { - const requestKey = buildRequestKey(status, dates.join(','), Date.now().toString()) + const requestKey = buildRequestKey( + status, + dates.join(','), + Date.now() + .toString(), + ) latestUpdateRequestRef.current = requestKey setIsUpdating(true) - setError(null) + setError(undefined) try { await setLeaveDatesService(dates, status) - } catch (error) { + } catch (err) { if (latestUpdateRequestRef.current === requestKey) { - setError(error) - handleError(error) - throw error + setError(err) + handleError(err) + throw err } } finally { if (latestUpdateRequestRef.current === requestKey) { diff --git a/src/apps/leave-tracker/src/lib/hooks/useFetchTeamLeave.ts b/src/apps/calendar/src/lib/hooks/useFetchTeamLeave.ts similarity index 78% rename from src/apps/leave-tracker/src/lib/hooks/useFetchTeamLeave.ts rename to src/apps/calendar/src/lib/hooks/useFetchTeamLeave.ts index 33f386726..6446241fc 100644 --- a/src/apps/leave-tracker/src/lib/hooks/useFetchTeamLeave.ts +++ b/src/apps/calendar/src/lib/hooks/useFetchTeamLeave.ts @@ -13,36 +13,45 @@ export interface UseFetchTeamLeaveResult { loadTeamLeave: (startDate?: Date, endDate?: Date) => Promise } -const buildRequestKey = (...parts: Array): string => - parts.filter(Boolean).join('|') +const buildRequestKey = (...parts: Array): string => ( + parts + .filter(Boolean) + .join('|') +) export function useFetchTeamLeave(): UseFetchTeamLeaveResult { const [teamLeaveDates, setTeamLeaveDates] = useState([]) const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) + const [error, setError] = useState(undefined) const latestLoadRequestRef = useRef('') const loadTeamLeave = useCallback( async (startDate?: Date, endDate?: Date) => { const startKey = startDate ? getDateKey(startDate) : undefined const endKey = endDate ? getDateKey(endDate) : undefined - const requestKey = buildRequestKey(startKey, endKey, Date.now().toString()) + const requestKey = buildRequestKey( + startKey, + endKey, + Date.now() + .toString(), + ) latestLoadRequestRef.current = requestKey setIsLoading(true) setTeamLeaveDates([]) - setError(null) + setError(undefined) try { const response = await fetchTeamLeave(startKey, endKey) if (latestLoadRequestRef.current !== requestKey) { return } + setTeamLeaveDates(response) - } catch (error) { + } catch (err) { if (latestLoadRequestRef.current === requestKey) { - setError(error) - handleError(error) - throw error + setError(err) + handleError(err) + throw err } } finally { if (latestLoadRequestRef.current === requestKey) { diff --git a/src/apps/leave-tracker/src/lib/index.ts b/src/apps/calendar/src/lib/index.ts similarity index 100% rename from src/apps/leave-tracker/src/lib/index.ts rename to src/apps/calendar/src/lib/index.ts diff --git a/src/apps/leave-tracker/src/lib/models/index.ts b/src/apps/calendar/src/lib/models/index.ts similarity index 85% rename from src/apps/leave-tracker/src/lib/models/index.ts rename to src/apps/calendar/src/lib/models/index.ts index 7fc6312e1..e993d3773 100644 --- a/src/apps/leave-tracker/src/lib/models/index.ts +++ b/src/apps/calendar/src/lib/models/index.ts @@ -19,10 +19,12 @@ export interface TeamLeaveDate { usersOnLeave: Array<{ userId: string handle?: string + firstName?: string + lastName?: string status: LeaveStatus.LEAVE | LeaveStatus.WIPRO_HOLIDAY }> } -export interface LeaveTrackerContextModel { +export interface CalendarContextModel { loginUserInfo?: TokenModel } diff --git a/src/apps/leave-tracker/src/lib/services/index.ts b/src/apps/calendar/src/lib/services/index.ts similarity index 100% rename from src/apps/leave-tracker/src/lib/services/index.ts rename to src/apps/calendar/src/lib/services/index.ts diff --git a/src/apps/leave-tracker/src/lib/services/leave.service.ts b/src/apps/calendar/src/lib/services/leave.service.ts similarity index 80% rename from src/apps/leave-tracker/src/lib/services/leave.service.ts rename to src/apps/calendar/src/lib/services/leave.service.ts index d1af84de1..4169e8138 100644 --- a/src/apps/leave-tracker/src/lib/services/leave.service.ts +++ b/src/apps/calendar/src/lib/services/leave.service.ts @@ -5,14 +5,16 @@ import { xhrGetAsync, xhrPostAsync } from '~/libs/core' import type { LeaveDate, LeaveUpdateStatus, TeamLeaveDate } from '../models' -const serializeQuery = (params: Record): string => - qs.stringify(params, { addQueryPrefix: true, skipNulls: true }) +const serializeQuery = (params: Record): string => qs.stringify( + params, + { addQueryPrefix: true, skipNulls: true }, +) export const fetchUserLeaveDates = async ( startDate?: string, endDate?: string, ): Promise => { - const queryString = serializeQuery({ startDate, endDate }) + const queryString = serializeQuery({ endDate, startDate }) return xhrGetAsync(`${EnvironmentConfig.API.V6}/leave/dates${queryString}`) } @@ -26,7 +28,7 @@ export const fetchTeamLeave = async ( startDate?: string, endDate?: string, ): Promise => { - const queryString = serializeQuery({ startDate, endDate }) + const queryString = serializeQuery({ endDate, startDate }) return xhrGetAsync(`${EnvironmentConfig.API.V6}/leave/team${queryString}`) } diff --git a/src/apps/leave-tracker/src/lib/styles/index.scss b/src/apps/calendar/src/lib/styles/index.scss similarity index 97% rename from src/apps/leave-tracker/src/lib/styles/index.scss rename to src/apps/calendar/src/lib/styles/index.scss index 7940d6ff5..6c1ffcaa9 100644 --- a/src/apps/leave-tracker/src/lib/styles/index.scss +++ b/src/apps/calendar/src/lib/styles/index.scss @@ -11,7 +11,7 @@ --team-cell-border: #e5e7eb; } -.leave-tracker-app { +.calendar-app { --status-available: #f0f0f0; --status-leave: #ff6b6b; --status-holiday: #ffd93d; diff --git a/src/apps/leave-tracker/src/lib/utils/calendar.utils.ts b/src/apps/calendar/src/lib/utils/calendar.utils.ts similarity index 92% rename from src/apps/leave-tracker/src/lib/utils/calendar.utils.ts rename to src/apps/calendar/src/lib/utils/calendar.utils.ts index 104f27d70..b1e6fd505 100644 --- a/src/apps/leave-tracker/src/lib/utils/calendar.utils.ts +++ b/src/apps/calendar/src/lib/utils/calendar.utils.ts @@ -43,9 +43,9 @@ export const getStatusForDate = (date: Date, leaveDates: LeaveDate[]): LeaveStat return match?.status ?? LeaveStatus.AVAILABLE } -export const getStatusColor = (status: LeaveStatus): string => { - return statusColorMap[status] ?? statusColorMap[LeaveStatus.AVAILABLE] -} +export const getStatusColor = (status: LeaveStatus): string => ( + statusColorMap[status] ?? statusColorMap[LeaveStatus.AVAILABLE] +) export const getStatusLabel = (status: LeaveStatus): string => statusLabelMap[status] diff --git a/src/apps/leave-tracker/src/lib/utils/index.ts b/src/apps/calendar/src/lib/utils/index.ts similarity index 100% rename from src/apps/leave-tracker/src/lib/utils/index.ts rename to src/apps/calendar/src/lib/utils/index.ts diff --git a/src/apps/leave-tracker/src/pages/personal-calendar/PersonalCalendarPage.module.scss b/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.module.scss similarity index 100% rename from src/apps/leave-tracker/src/pages/personal-calendar/PersonalCalendarPage.module.scss rename to src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.module.scss diff --git a/src/apps/leave-tracker/src/pages/personal-calendar/PersonalCalendarPage.tsx b/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx similarity index 87% rename from src/apps/leave-tracker/src/pages/personal-calendar/PersonalCalendarPage.tsx rename to src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx index 866027b60..f0a70c87b 100644 --- a/src/apps/leave-tracker/src/pages/personal-calendar/PersonalCalendarPage.tsx +++ b/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx @@ -4,23 +4,23 @@ import { FC, useCallback, useContext, useEffect, useMemo, useState } from 'react import { Button } from '~/libs/ui' import { Calendar, CalendarLegend, MonthNavigation } from '../../lib/components' -import { LeaveStatus } from '../../lib/models' -import { LeaveTrackerContext } from '../../lib/contexts/LeaveTrackerContext' +import { CalendarContext } from '../../lib/contexts/CalendarContext' import { useFetchLeaveDates } from '../../lib/hooks' +import { LeaveStatus } from '../../lib/models' + import styles from './PersonalCalendarPage.module.scss' const PersonalCalendarPage: FC = () => { - const { loginUserInfo } = useContext(LeaveTrackerContext) + const calendarContext = useContext(CalendarContext) const [currentDate, setCurrentDate] = useState(new Date()) const [selectedDates, setSelectedDates] = useState>(new Set()) - const { - error, - isLoading, - isUpdating, - leaveDates, - loadLeaveDates, - updateLeaveDates, - } = useFetchLeaveDates() + const leaveDatesState = useFetchLeaveDates() + const error = leaveDatesState.error + const isLoading = leaveDatesState.isLoading + const isUpdating = leaveDatesState.isUpdating + const leaveDates = leaveDatesState.leaveDates + const loadLeaveDates = leaveDatesState.loadLeaveDates + const updateLeaveDates = leaveDatesState.updateLeaveDates const [actionError, setActionError] = useState('') const loadCurrentMonth = useCallback(async () => { @@ -36,7 +36,7 @@ const PersonalCalendarPage: FC = () => { }, [currentDate, loadLeaveDates]) useEffect(() => { - void loadCurrentMonth() + loadCurrentMonth() }, [loadCurrentMonth]) const handlePrevMonth = useCallback(() => { @@ -57,6 +57,7 @@ const PersonalCalendarPage: FC = () => { } else { next.add(dateKey) } + return next }) }, []) @@ -99,7 +100,9 @@ const PersonalCalendarPage: FC = () => {

Welcome back

-

{loginUserInfo?.handle ?? 'Your calendar'}

+

+ {calendarContext.loginUserInfo?.handle ?? 'Your calendar'} +

diff --git a/src/apps/calendar/src/pages/personal-calendar/index.ts b/src/apps/calendar/src/pages/personal-calendar/index.ts new file mode 100644 index 000000000..5141e08f7 --- /dev/null +++ b/src/apps/calendar/src/pages/personal-calendar/index.ts @@ -0,0 +1 @@ +export { default as PersonalCalendarPage } from './PersonalCalendarPage' diff --git a/src/apps/leave-tracker/src/pages/team-calendar/TeamCalendarPage.module.scss b/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.module.scss similarity index 100% rename from src/apps/leave-tracker/src/pages/team-calendar/TeamCalendarPage.module.scss rename to src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.module.scss diff --git a/src/apps/leave-tracker/src/pages/team-calendar/TeamCalendarPage.tsx b/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.tsx similarity index 89% rename from src/apps/leave-tracker/src/pages/team-calendar/TeamCalendarPage.tsx rename to src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.tsx index 06895b2ea..ab50c6d28 100644 --- a/src/apps/leave-tracker/src/pages/team-calendar/TeamCalendarPage.tsx +++ b/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.tsx @@ -9,7 +9,11 @@ import styles from './TeamCalendarPage.module.scss' const TeamCalendarPage: FC = () => { const [currentDate, setCurrentDate] = useState(new Date()) const [actionError, setActionError] = useState('') - const { error, isLoading, loadTeamLeave, teamLeaveDates } = useFetchTeamLeave() + const teamLeaveState = useFetchTeamLeave() + const error = teamLeaveState.error + const isLoading = teamLeaveState.isLoading + const loadTeamLeave = teamLeaveState.loadTeamLeave + const teamLeaveDates = teamLeaveState.teamLeaveDates const loadCurrentMonth = useCallback(async () => { setActionError('') @@ -24,7 +28,7 @@ const TeamCalendarPage: FC = () => { }, [currentDate, loadTeamLeave]) useEffect(() => { - void loadCurrentMonth() + loadCurrentMonth() }, [loadCurrentMonth]) const handlePrevMonth = useCallback(() => { diff --git a/src/apps/calendar/src/pages/team-calendar/index.ts b/src/apps/calendar/src/pages/team-calendar/index.ts new file mode 100644 index 000000000..be8c78227 --- /dev/null +++ b/src/apps/calendar/src/pages/team-calendar/index.ts @@ -0,0 +1 @@ +export { default as TeamCalendarPage } from './TeamCalendarPage' diff --git a/src/apps/leave-tracker/src/config/index.config.ts b/src/apps/leave-tracker/src/config/index.config.ts deleted file mode 100644 index 21fb5a95c..000000000 --- a/src/apps/leave-tracker/src/config/index.config.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Common leave tracker config constants. - */ -export const APP_NAME = 'Leave Tracker' diff --git a/src/apps/leave-tracker/src/index.ts b/src/apps/leave-tracker/src/index.ts deleted file mode 100644 index 59efccaa5..000000000 --- a/src/apps/leave-tracker/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { leaveTrackerRoutes } from './leave-tracker-app.routes' -export { rootRoute as leaveTrackerRootRoute } from './config/routes.config' diff --git a/src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.tsx b/src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.tsx deleted file mode 100644 index 9aa8dbae7..000000000 --- a/src/apps/leave-tracker/src/lib/components/TeamCalendar/TeamCalendar.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import classNames from 'classnames' -import { isWeekend } from 'date-fns' -import { FC, useMemo } from 'react' - -import { LoadingSpinner } from '~/libs/ui' - -import { LeaveStatus, TeamLeaveDate } from '../../models' -import { getDateKey, getMonthDates } from '../../utils' - -import styles from './TeamCalendar.module.scss' - -const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] - -interface TeamCalendarProps { - currentDate: Date - teamLeaveDates: TeamLeaveDate[] - isLoading: boolean -} - -export const TeamCalendar: FC = ({ - currentDate, - isLoading, - teamLeaveDates, -}) => { - const monthDates = useMemo( - () => getMonthDates(currentDate.getFullYear(), currentDate.getMonth()), - [currentDate], - ) - - const paddedDates = useMemo(() => { - if (!monthDates.length) { - return [] - } - - const padding = monthDates[0].getDay() - const cells: Array = [] - - for (let i = 0; i < padding; i += 1) { - cells.push(null) - } - - cells.push(...monthDates) - - while (cells.length % 7 !== 0) { - cells.push(null) - } - - return cells - }, [monthDates]) - - return ( -
-
- {dayNames.map(day => ( -
- {day} -
- ))} -
- -
- {paddedDates.map((date, index) => { - if (!date) { - return
- } - - const dateKey = getDateKey(date) - const leaveEntry = teamLeaveDates.find(item => item.date === dateKey) - const users = leaveEntry?.usersOnLeave ?? [] - const displayedUsers = users.slice(0, 10) - const overflowCount = users.length - displayedUsers.length - const weekendClass = isWeekend(date) ? styles.weekend : undefined - - return ( -
- {date.getDate()} -
- {displayedUsers.length ? ( - displayedUsers.map((user, userIndex) => ( -
- {user.handle ?? user.userId} -
- )) - ) : ( -
No leave
- )} - {overflowCount > 0 && ( -
- +{overflowCount} more -
- )} -
-
- ) - })} -
- - {isLoading && ( -
- -
- )} -
- ) -} - -export default TeamCalendar diff --git a/src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContext.ts b/src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContext.ts deleted file mode 100644 index dba938fd8..000000000 --- a/src/apps/leave-tracker/src/lib/contexts/LeaveTrackerContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createContext } from 'react' - -import type { LeaveTrackerContextModel } from '../models' - -export const LeaveTrackerContext = createContext({ - loginUserInfo: undefined, -}) - -export default LeaveTrackerContext diff --git a/src/apps/leave-tracker/src/lib/contexts/index.ts b/src/apps/leave-tracker/src/lib/contexts/index.ts deleted file mode 100644 index 3170d57be..000000000 --- a/src/apps/leave-tracker/src/lib/contexts/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './LeaveTrackerContext' -export * from './LeaveTrackerContextProvider' -export * from './SWRConfigProvider' diff --git a/src/apps/leave-tracker/src/pages/personal-calendar/index.ts b/src/apps/leave-tracker/src/pages/personal-calendar/index.ts deleted file mode 100644 index 94d4b12da..000000000 --- a/src/apps/leave-tracker/src/pages/personal-calendar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './PersonalCalendarPage' diff --git a/src/apps/leave-tracker/src/pages/team-calendar/index.ts b/src/apps/leave-tracker/src/pages/team-calendar/index.ts deleted file mode 100644 index 6aecf3055..000000000 --- a/src/apps/leave-tracker/src/pages/team-calendar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './TeamCalendarPage' diff --git a/src/apps/platform/src/platform.routes.tsx b/src/apps/platform/src/platform.routes.tsx index 89d2b78cb..0eb08ac11 100644 --- a/src/apps/platform/src/platform.routes.tsx +++ b/src/apps/platform/src/platform.routes.tsx @@ -11,7 +11,7 @@ import { walletAdminRoutes } from '~/apps/wallet-admin' import { copilotsRoutes } from '~/apps/copilots' import { adminRoutes } from '~/apps/admin' import { reviewRoutes } from '~/apps/review' -import { leaveTrackerRoutes } from '~/apps/leave-tracker' +import { calendarRoutes } from '~/apps/calendar' const Home: LazyLoadedComponent = lazyLoad( () => import('./routes/home'), @@ -40,7 +40,7 @@ export const platformRoutes: Array = [ ...walletAdminRoutes, ...accountsRoutes, ...reviewRoutes, - ...leaveTrackerRoutes, + ...calendarRoutes, ...homeRoutes, ...adminRoutes, ] diff --git a/src/config/constants.ts b/src/config/constants.ts index 1f7ab2a90..6e5c336c6 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -12,7 +12,7 @@ export enum AppSubdomain { copilots = 'copilots', admin = 'system-admin', review = 'review', - leaveTracker = 'leave-tracker', + calendar = 'calendar', } export enum ToolTitle { @@ -29,7 +29,7 @@ export enum ToolTitle { copilots = 'Copilots', admin = 'Admin', review = 'Review', - leaveTracker = 'Leave Tracker', + calendar = 'Calendar', } export const PageSubheaderPortalId: string = 'page-subheader-portal-el' From 5d3d35ebf162633d728a1d1943778e44df69ca9d Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 6 Jan 2026 10:47:18 +1100 Subject: [PATCH 03/11] Lint --- .../components/TeamCalendar/TeamCalendar.tsx | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx b/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx index d5cecce4e..723be6b0c 100644 --- a/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx +++ b/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx @@ -15,7 +15,9 @@ type TeamLeaveUser = TeamLeaveDate['usersOnLeave'][number] const getUserDisplayName = (user: TeamLeaveUser): string => { const firstName = user.firstName?.trim() const lastName = user.lastName?.trim() - const fullName = [firstName, lastName].filter(Boolean).join(' ') + const fullName = [firstName, lastName] + .filter(Boolean) + .join(' ') return fullName || user.handle || user.userId } @@ -115,21 +117,20 @@ export const TeamCalendar: FC = (props: TeamCalendarProps) => > {date.getDate()}
- {displayedUsers.length - ? displayedUsers.map((user, userIndex) => ( -
- {getUserDisplayName(user)} -
- )) - : null} + {displayedUsers.length > 0 + && displayedUsers.map((user, userIndex) => ( +
+ {getUserDisplayName(user)} +
+ ))} {overflowCount > 0 && (
{`+${overflowCount} more`} From 6e0d13c2e0d1569517c4784828e9fc22068060a1 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 8 Jan 2026 09:37:32 +1100 Subject: [PATCH 04/11] Send scheduledEndDate value instead of duration when extending a phase --- .../lib/services/challenge-phases.service.ts | 1 + .../ChallengeDetailsPage.tsx | 25 +++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/apps/review/src/lib/services/challenge-phases.service.ts b/src/apps/review/src/lib/services/challenge-phases.service.ts index 7a963bf9b..60cd15e06 100644 --- a/src/apps/review/src/lib/services/challenge-phases.service.ts +++ b/src/apps/review/src/lib/services/challenge-phases.service.ts @@ -9,6 +9,7 @@ const challengePhaseBaseUrl = `${EnvironmentConfig.API.V6}` export interface UpdateChallengePhaseRequest { isOpen: boolean duration?: number + scheduledEndDate?: string } /** diff --git a/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx index 6cc775396..f4581f74b 100644 --- a/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx @@ -1390,12 +1390,27 @@ export const ChallengeDetailsPage: FC = (props: Props) => { return } - const totalSeconds = Math.ceil(totalSecondsFloat) + const currentEndMoment = (() => { + const endSource = extendTarget.actualEndDate ?? extendTarget.scheduledEndDate + if (endSource) { + const parsed = moment(endSource) + if (parsed.isValid()) { + return parsed + } + } + + const currentDuration = extendTarget.duration + if (typeof currentDuration === 'number' && Number.isFinite(currentDuration)) { + return startMoment + .clone() + .add(Math.max(currentDuration, 0), 'seconds') + } - const currentDuration = extendTarget.duration ?? 0 + return undefined + })() - if (totalSeconds <= currentDuration) { - setExtendError('New end date must extend the phase beyond the current duration.') + if (currentEndMoment && !endMoment.isAfter(currentEndMoment)) { + setExtendError('New end date must extend the phase beyond the current end date.') return } @@ -1404,8 +1419,8 @@ export const ChallengeDetailsPage: FC = (props: Props) => { const didSucceed = await handlePhaseUpdate( extendTarget.id, { - duration: totalSeconds, isOpen: true, + scheduledEndDate: endMoment.toISOString(), }, { error: `Failed to extend ${extendTarget.name} phase.`, From fa1531110dcce9a8535f840d1c7a6e0296a17bbf Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 12 Jan 2026 10:48:10 +0200 Subject: [PATCH 05/11] Fix submissions visibility for approvers & checkpoint-reviewers --- .../ChallengeDetailsContent/TabContentCheckpoint.tsx | 3 ++- .../components/ChallengeDetailsContent/TabContentReview.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentCheckpoint.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentCheckpoint.tsx index 2f1ed0b9f..144fcbb35 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentCheckpoint.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentCheckpoint.tsx @@ -37,6 +37,7 @@ export const TabContentCheckpoint: FC = (props: Props) => { checkpointScreenerResourceIds, isPrivilegedRole, hasCheckpointScreenerRole, + hasCheckpointReviewerRole, }: useRoleProps = useRole() const myMemberIds = useMemo>( @@ -105,7 +106,7 @@ export const TabContentCheckpoint: FC = (props: Props) => { () => { const baseRows = props.checkpoint ?? [] - const canSeeAll = isPrivilegedRole || hasCheckpointScreenerRole + const canSeeAll = isPrivilegedRole || hasCheckpointScreenerRole || hasCheckpointReviewerRole if (canSeeAll || (isChallengeCompleted && hasPassedCheckpointScreeningThreshold)) { return baseRows } diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentReview.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentReview.tsx index 25721f38b..14d170a6e 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentReview.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentReview.tsx @@ -176,7 +176,7 @@ export const TabContentReview: FC = (props: Props) => { resourceMemberIdMapping, resources, }: ChallengeDetailContextModel = useContext(ChallengeDetailContext) - const { actionChallengeRole, isPrivilegedRole }: useRoleProps = useRole() + const { actionChallengeRole, isPrivilegedRole, hasApproverRole }: useRoleProps = useRole() const challengeSubmissions = useMemo( () => challengeInfo?.submissions ?? [], [challengeInfo?.submissions], @@ -524,7 +524,7 @@ export const TabContentReview: FC = (props: Props) => { const validReviewPhaseSubmissions = baseReviews.filter(hasReviewPhaseReview) - if (isPrivilegedRole || (isChallengeCompleted && hasPassedReviewThreshold)) { + if (isPrivilegedRole || hasApproverRole || (isChallengeCompleted && hasPassedReviewThreshold)) { return validReviewPhaseSubmissions } From 6186a0ef97796cf7bb1d15f629c6c70f111e186f Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 12 Jan 2026 20:43:31 +0530 Subject: [PATCH 06/11] PM-3352 Validate skills during onboarding --- .../onboarding/src/pages/skills/index.tsx | 20 ++++++++++++++++++- .../src/pages/skills/styles.module.scss | 5 +++++ .../use-member-skill-editor.tsx | 12 +++++++++-- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/apps/onboarding/src/pages/skills/index.tsx b/src/apps/onboarding/src/pages/skills/index.tsx index 064e02cc7..9ac5a1606 100644 --- a/src/apps/onboarding/src/pages/skills/index.tsx +++ b/src/apps/onboarding/src/pages/skills/index.tsx @@ -1,5 +1,5 @@ import { useNavigate } from 'react-router-dom' -import { FC, useState } from 'react' +import { FC, useEffect, useState } from 'react' import { connect } from 'react-redux' import classNames from 'classnames' @@ -17,8 +17,15 @@ export const PageSkillsContent: FC<{ const navigate: any = useNavigate() const [loading, setLoading] = useState(false) const editor: MemberSkillEditor = useMemberSkillEditor() + const [showValidationError, setShowValidationError] = useState(false) async function saveSkills(): Promise { + if (!editor.hasValidSkills()) { + setShowValidationError(true) + return + } + + setShowValidationError(false) setLoading(true) try { await editor.saveSkills() @@ -29,6 +36,12 @@ export const PageSkillsContent: FC<{ navigate('../open-to-work') } + useEffect(() => { + if (editor.hasValidSkills()) { + setShowValidationError(false) + } + }, [editor]) + return (

@@ -56,6 +69,11 @@ export const PageSkillsContent: FC<{ progress={1} maxStep={5} /> + {showValidationError && ( + + * Please select at least one skill in both Principal and Additional Skills. + + )}
- - I will complete this onboarding later, - skip for now - . - ) } diff --git a/src/apps/onboarding/src/pages/onboarding/styles.module.scss b/src/apps/onboarding/src/pages/onboarding/styles.module.scss index 6b05aae3e..585282491 100644 --- a/src/apps/onboarding/src/pages/onboarding/styles.module.scss +++ b/src/apps/onboarding/src/pages/onboarding/styles.module.scss @@ -68,19 +68,3 @@ text-transform: none; } } - -.textFooter { - color: $black-80; - margin-top: 64px; - text-align: center; - - @include ltemd { - margin-top: 32px; - max-width: 284px; - } - - a { - color: $turq-160; - font-weight: 500; - } -} \ No newline at end of file From b1ee60e8d35e0112c39682a787623a2fa7a9c70e Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 12 Jan 2026 21:02:35 +0530 Subject: [PATCH 09/11] fix linting error --- src/apps/onboarding/src/pages/onboarding/index.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/apps/onboarding/src/pages/onboarding/index.tsx b/src/apps/onboarding/src/pages/onboarding/index.tsx index cf696cf04..525fd7485 100644 --- a/src/apps/onboarding/src/pages/onboarding/index.tsx +++ b/src/apps/onboarding/src/pages/onboarding/index.tsx @@ -1,12 +1,10 @@ import { FC, useContext, useEffect } from 'react' import { Outlet, Routes, useLocation } from 'react-router-dom' -import { Provider, useDispatch, useSelector } from 'react-redux' +import { Provider, useDispatch } from 'react-redux' import classNames from 'classnames' import { routerContext, RouterContextData } from '~/libs/core' -import { Member } from '~/apps/talent-search/src/lib/models' import { SharedSwrConfig } from '~/libs/shared' -import { EnvironmentConfig } from '~/config' import { onboardRouteId } from '../../onboarding.routes' import { fetchMemberInfo, fetchMemberTraits } from '../../redux/actions/member' @@ -20,7 +18,6 @@ const OnboardingContent: FC<{ const { getChildRoutes }: RouterContextData = useContext(routerContext) const location = useLocation() const dispatch = useDispatch() - const reduxMemberInfo: Member = useSelector((state: any) => state.member.memberInfo) useEffect(() => { dispatch(fetchMemberInfo()) From 26ff8528776502a289f749caa215bf44bd06cdf0 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 13 Jan 2026 10:18:40 +0200 Subject: [PATCH 10/11] PM-2631 - allow checkpoint reviewers to see reviews --- .../ChallengeDetailsContent/TabContentReview.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentReview.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentReview.tsx index 14d170a6e..a569f289f 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentReview.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentReview.tsx @@ -222,8 +222,12 @@ export const TabContentReview: FC = (props: Props) => { }, [challengeInfo?.status], ) + + const isSubmitterView = actionChallengeRole === SUBMITTER + && selectedTab !== APPROVAL + const hasPassedReviewThreshold = useMemo( - () => hasSubmitterPassedThreshold( + () => !isSubmitterView || hasSubmitterPassedThreshold( providedReviews ?? [], myOwnedMemberIds, props.reviewMinimumPassingScore, @@ -673,8 +677,6 @@ export const TabContentReview: FC = (props: Props) => { && actionChallengeRole === REVIEWER // show loading ui when fetching data - const isSubmitterView = actionChallengeRole === SUBMITTER - && selectedTab !== APPROVAL const reviewRows = isSubmitterView ? (shouldSortReviewTabByScore ? submitterRowsForReviewTab : filteredSubmitterReviews) : (shouldSortReviewTabByScore ? reviewerRowsForReviewTab : filteredReviews) From 06c13cea1ad5185e7bc1f6cd469c099ad74e9281 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 14 Jan 2026 17:04:02 +1100 Subject: [PATCH 11/11] Allow users to set their own holidays --- .../components/TeamCalendar/TeamCalendar.tsx | 31 +++++++++++-------- src/apps/calendar/src/lib/models/index.ts | 7 +++-- .../calendar/src/lib/utils/calendar.utils.ts | 3 ++ .../PersonalCalendarPage.tsx | 21 +++++++++++++ 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx b/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx index 723be6b0c..1a2feecea 100644 --- a/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx +++ b/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx @@ -118,19 +118,24 @@ export const TeamCalendar: FC = (props: TeamCalendarProps) => {date.getDate()}
{displayedUsers.length > 0 - && displayedUsers.map((user, userIndex) => ( -
- {getUserDisplayName(user)} -
- ))} + && displayedUsers.map((user, userIndex) => { + const isHolidayStatus = user.status === LeaveStatus.WIPRO_HOLIDAY + || user.status === LeaveStatus.HOLIDAY + + return ( +
+ {getUserDisplayName(user)} +
+ ) + })} {overflowCount > 0 && (
{`+${overflowCount} more`} diff --git a/src/apps/calendar/src/lib/models/index.ts b/src/apps/calendar/src/lib/models/index.ts index e993d3773..994fa9c6c 100644 --- a/src/apps/calendar/src/lib/models/index.ts +++ b/src/apps/calendar/src/lib/models/index.ts @@ -3,11 +3,12 @@ import { TokenModel } from '~/libs/core' export enum LeaveStatus { AVAILABLE = 'AVAILABLE', LEAVE = 'LEAVE', - WEEKEND = 'WEEKEND', + HOLIDAY = 'HOLIDAY', WIPRO_HOLIDAY = 'WIPRO_HOLIDAY', + WEEKEND = 'WEEKEND', } -export type LeaveUpdateStatus = LeaveStatus.AVAILABLE | LeaveStatus.LEAVE +export type LeaveUpdateStatus = LeaveStatus.AVAILABLE | LeaveStatus.LEAVE | LeaveStatus.HOLIDAY export interface LeaveDate { date: string @@ -21,7 +22,7 @@ export interface TeamLeaveDate { handle?: string firstName?: string lastName?: string - status: LeaveStatus.LEAVE | LeaveStatus.WIPRO_HOLIDAY + status: LeaveStatus.LEAVE | LeaveStatus.HOLIDAY | LeaveStatus.WIPRO_HOLIDAY }> } diff --git a/src/apps/calendar/src/lib/utils/calendar.utils.ts b/src/apps/calendar/src/lib/utils/calendar.utils.ts index b1e6fd505..b9a90153f 100644 --- a/src/apps/calendar/src/lib/utils/calendar.utils.ts +++ b/src/apps/calendar/src/lib/utils/calendar.utils.ts @@ -4,6 +4,7 @@ import { LeaveDate, LeaveStatus } from '../models' const statusColorMap: Record = { [LeaveStatus.LEAVE]: 'status-leave', + [LeaveStatus.HOLIDAY]: 'status-holiday', [LeaveStatus.WIPRO_HOLIDAY]: 'status-holiday', [LeaveStatus.WEEKEND]: 'status-weekend', [LeaveStatus.AVAILABLE]: 'status-available', @@ -11,6 +12,7 @@ const statusColorMap: Record = { const statusLabelMap: Record = { [LeaveStatus.LEAVE]: 'Leave', + [LeaveStatus.HOLIDAY]: 'Personal Holiday', [LeaveStatus.WIPRO_HOLIDAY]: 'Wipro Holiday', [LeaveStatus.WEEKEND]: 'Weekend', [LeaveStatus.AVAILABLE]: 'Available', @@ -19,6 +21,7 @@ const statusLabelMap: Record = { export const legendStatusOrder: LeaveStatus[] = [ LeaveStatus.AVAILABLE, LeaveStatus.LEAVE, + LeaveStatus.HOLIDAY, LeaveStatus.WIPRO_HOLIDAY, LeaveStatus.WEEKEND, ] diff --git a/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx b/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx index f0a70c87b..fe2ab14b9 100644 --- a/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx +++ b/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx @@ -75,6 +75,19 @@ const PersonalCalendarPage: FC = () => { } }, [loadCurrentMonth, selectedDates, updateLeaveDates]) + const handleSetAsHoliday = useCallback(async () => { + if (!selectedDates.size) return + + setActionError('') + try { + await updateLeaveDates(Array.from(selectedDates), LeaveStatus.HOLIDAY) + setSelectedDates(new Set()) + await loadCurrentMonth() + } catch { + setActionError('Unable to update leave dates. Please try again.') + } + }, [loadCurrentMonth, selectedDates, updateLeaveDates]) + const handleSetAsAvailable = useCallback(async () => { if (!selectedDates.size) return @@ -133,6 +146,14 @@ const PersonalCalendarPage: FC = () => { > Set as Leave +