Skip to content

Commit 7ca095d

Browse files
Reapply "Merge branch 'develop' into feature/gantt-chart-improvements"
This reverts commit 8ba3a06.
1 parent eda7d61 commit 7ca095d

11 files changed

Lines changed: 352 additions & 28 deletions

File tree

src/backend/src/controllers/calendar.controllers.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,4 +596,18 @@ export default class CalendarController {
596596
next(error);
597597
}
598598
}
599+
600+
static async getAllEventsPaginated(req: Request, res: Response, next: NextFunction) {
601+
try {
602+
const { cursor, pageSize } = req.body;
603+
const paginatedEvents = await CalendarService.getAllEventsPaginated(
604+
req.organization,
605+
cursor ? new Date(cursor) : undefined,
606+
pageSize ? parseInt(pageSize) : undefined
607+
);
608+
res.status(200).json(paginatedEvents);
609+
} catch (error: unknown) {
610+
next(error);
611+
}
612+
}
599613
}

src/backend/src/routes/calendar.routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,5 +297,6 @@ calendarRouter.post(
297297
);
298298

299299
calendarRouter.get('/calendars', CalendarController.getAllCalendars);
300+
calendarRouter.post('/events-paginated', CalendarController.getAllEventsPaginated);
300301

301302
export default calendarRouter;

src/backend/src/services/calendar.services.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
Machinery,
1616
ScheduleSlot,
1717
notGuest,
18-
isSameDay
18+
isSameDay,
19+
EventInstance
1920
} from 'shared';
2021
import { getCalendarQueryArgs } from '../prisma-query-args/calendar.query-args.js';
2122
import { getEventTypeQueryArgs } from '../prisma-query-args/event-type.query-args.js';
@@ -2749,4 +2750,56 @@ export default class CalendarService {
27492750
});
27502751
return eventTypes.map(eventTypeTransformer);
27512752
}
2753+
2754+
/**
2755+
* Gets all the events paginated, ordered by start time and grouped by date
2756+
* @param organization the org the user is currently in
2757+
* @param cursor the start time of the last event on the prev page
2758+
* @param pageSize the number of events to return per page
2759+
* @returns
2760+
*/
2761+
static async getAllEventsPaginated(
2762+
organization: Organization,
2763+
cursor?: Date,
2764+
pageSize: number = 25
2765+
): Promise<{ instances: EventInstance[]; nextCursor: Date | null }> {
2766+
const now = new Date();
2767+
2768+
const slots = await prisma.schedule_Slot.findMany({
2769+
where: {
2770+
startTime: {
2771+
lt: cursor ?? now
2772+
},
2773+
event: {
2774+
dateDeleted: null,
2775+
status: Event_Status.SCHEDULED,
2776+
eventType: {
2777+
organizationId: organization.organizationId
2778+
}
2779+
}
2780+
},
2781+
include: {
2782+
event: getEventQueryArgs(organization.organizationId)
2783+
},
2784+
orderBy: { startTime: 'desc' },
2785+
take: pageSize
2786+
});
2787+
2788+
const nextCursor = slots.length === pageSize ? slots[slots.length - 1].startTime : null;
2789+
2790+
const instances: EventInstance[] = slots.map((slot) => {
2791+
const { scheduledTimes, ...eventWithoutSlots } = eventTransformer(slot.event);
2792+
return {
2793+
...eventWithoutSlots,
2794+
scheduleSlotId: slot.scheduleSlotId,
2795+
startTime: slot.startTime,
2796+
endTime: slot.endTime,
2797+
allDay: slot.allDay,
2798+
recurring: slot.event.scheduledTimes.length > 1,
2799+
totalScheduledSlots: slot.event.scheduledTimes.length
2800+
};
2801+
});
2802+
2803+
return { instances, nextCursor };
2804+
}
27522805
}

src/frontend/src/apis/calendar.api.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
EventTypeCreateArgs,
1212
Calendar,
1313
FilterArgs,
14-
ScheduleSlot
14+
ScheduleSlot,
15+
EventInstance
1516
} from 'shared';
1617
import { eventTransformer, eventWithMembersTransformer } from './transformers/calendar.transformer';
1718
import { EditEventArgs, EditScheduleSlotArgs, EventCreateArgs } from '../hooks/calendar.hooks';
@@ -266,3 +267,19 @@ export const scheduleEvent = async (eventId: string, payload: { startTime: Date;
266267
transformResponse: (data) => eventTransformer(JSON.parse(data))
267268
});
268269
};
270+
271+
export const getAllEventsPaginated = (cursor?: Date, pageSize?: number) => {
272+
return axios.post<{ instances: EventInstance[]; nextCursor: Date | null }>(
273+
apiUrls.calendarEventsPaginated(),
274+
{ cursor, pageSize },
275+
{
276+
transformResponse: (data) => {
277+
const parsed = JSON.parse(data);
278+
return {
279+
instances: parsed.instances,
280+
nextCursor: parsed.nextCursor ? new Date(parsed.nextCursor) : null
281+
};
282+
}
283+
}
284+
);
285+
};

src/frontend/src/app/AppAuthenticated.tsx

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { useCurrentOrganization } from '../hooks/organizations.hooks';
3434
import Statistics from '../pages/StatisticsPage/Statistics';
3535
import RetrospectiveGanttChartPage from '../pages/RetrospectivePage/Retrospective';
3636
import Calendar from '../pages/CalendarPage/Calendar';
37+
import GuestEventPage from '../pages/GuestEventPage/GuestEventPage';
3738

3839
interface AppAuthenticatedProps {
3940
userId: string;
@@ -121,26 +122,72 @@ const AppAuthenticated: React.FC<AppAuthenticatedProps> = ({ userId, userRole })
121122

122123
return userSettingsData.slackId || isGuest(userRole) ? (
123124
<AppContextUser>
124-
<SidebarLayout>
125-
<Switch>
126-
<Route path={routes.PROJECTS} component={Projects} />
127-
<Redirect from={routes.CR_BY_ID} to={routes.CHANGE_REQUESTS_BY_ID} />
128-
<Route path={routes.CHANGE_REQUESTS} component={ChangeRequests} />
129-
<Route path={routes.GANTT} component={GanttChartPage} />
130-
<Route path={routes.TEAMS} component={Teams} />
131-
<Route path={routes.SETTINGS} component={Settings} />
132-
<Route path={routes.ADMIN_TOOLS} component={AdminTools} />
133-
<Route path={routes.INFO} component={InfoPage} />
134-
<Route path={routes.CREDITS} component={Credits} />
135-
<Route path={routes.FINANCE} component={Finance} />
136-
<Route path={routes.CALENDAR} component={Calendar} />
137-
<Route path={routes.STATISTICS} component={Statistics} />
138-
<Route path={routes.HOME} component={Home} />
139-
<Route path={routes.RETROSPECTIVE} component={RetrospectiveGanttChartPage} />
140-
<Redirect from={routes.BASE} to={routes.HOME} />
141-
<Route path="*" component={PageNotFound} />
142-
</Switch>
143-
</SidebarLayout>
125+
{
126+
<>
127+
<Box
128+
onMouseEnter={() => {
129+
setDrawerOpen(true);
130+
}}
131+
sx={{
132+
height: '100vh',
133+
position: 'fixed',
134+
width: 15,
135+
borderRight: 2,
136+
borderRightColor: theme.palette.background.paper
137+
}}
138+
/>
139+
<IconButton
140+
onClick={() => {
141+
setDrawerOpen(true);
142+
setMoveContent(true);
143+
}}
144+
sx={{ position: 'fixed', left: -8, top: '3%' }}
145+
id="sidebar-button"
146+
>
147+
<ArrowCircleRightTwoToneIcon
148+
sx={{
149+
fontSize: '30px',
150+
zIndex: 1,
151+
'& path:first-of-type': { color: '#000000' },
152+
'& path:last-of-type': { color: '#ef4345' }
153+
}}
154+
/>
155+
</IconButton>
156+
<Sidebar
157+
drawerOpen={drawerOpen}
158+
setDrawerOpen={setDrawerOpen}
159+
moveContent={moveContent}
160+
setMoveContent={setMoveContent}
161+
/>
162+
</>
163+
}
164+
<Box display={'flex'}>
165+
<HiddenContentMargin open={moveContent} variant="permanent" />
166+
<Container
167+
maxWidth={false}
168+
sx={{ width: onGuestHomePage && moveContent ? 'calc(100vw - 220px)' : `calc(100vw - 30px)` }}
169+
>
170+
<Switch>
171+
<Route path={routes.PROJECTS} component={Projects} />
172+
<Redirect from={routes.CR_BY_ID} to={routes.CHANGE_REQUESTS_BY_ID} />
173+
<Route path={routes.CHANGE_REQUESTS} component={ChangeRequests} />
174+
<Route path={routes.GANTT} component={GanttChartPage} />
175+
<Route path={routes.TEAMS} component={Teams} />
176+
<Route path={routes.SETTINGS} component={Settings} />
177+
<Route path={routes.ADMIN_TOOLS} component={AdminTools} />
178+
<Route path={routes.INFO} component={InfoPage} />
179+
<Route path={routes.CREDITS} component={Credits} />
180+
<Route path={routes.FINANCE} component={Finance} />
181+
<Route path={routes.CALENDAR} component={Calendar} />
182+
<Route path={routes.STATISTICS} component={Statistics} />
183+
<Route path={routes.HOME} component={Home} />
184+
<Route path={routes.RETROSPECTIVE} component={RetrospectiveGanttChartPage} />
185+
<Route path={routes.EVENTS} component={GuestEventPage} />
186+
<Redirect from={routes.BASE} to={routes.HOME} />
187+
<Route path="*" component={PageNotFound} />
188+
</Switch>
189+
</Container>
190+
</Box>
144191
</AppContextUser>
145192
) : (
146193
<SetUserPreferences userSettings={userSettingsData} />

src/frontend/src/hooks/calendar.hooks.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
FilterArgs,
1212
ScheduleSlotCreateArgs,
1313
EventWithMembers,
14-
ScheduleSlot
14+
ScheduleSlot,
15+
EventInstance
1516
} from 'shared';
1617
import {
1718
getAllShops,
@@ -48,7 +49,8 @@ import {
4849
getSingleEventWithMembers,
4950
previewScheduleSlotRecurringEdits,
5051
postDeleteScheduleSlot,
51-
scheduleEvent
52+
scheduleEvent,
53+
getAllEventsPaginated
5254
} from '../apis/calendar.api';
5355
import { useCurrentUser } from './users.hooks';
5456
import { PDFDocument } from 'pdf-lib';
@@ -664,3 +666,16 @@ export const combinePdfsAndDownload = async (blobData: Blob[], filename: string)
664666
const pdfBlob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
665667
saveAs(pdfBlob, filename);
666668
};
669+
670+
/**
671+
* Custom hook to get all events in a paginated manner, sorted by scheduled date ascending.
672+
*/
673+
export const useAllEventsPaginated = (cursor?: Date, pageSize?: number) => {
674+
return useQuery<{ instances: EventInstance[]; nextCursor: Date | null }, Error>(
675+
['events', 'paginated', cursor, pageSize],
676+
async () => {
677+
const { data } = await getAllEventsPaginated(cursor, pageSize);
678+
return data;
679+
}
680+
);
681+
};

src/frontend/src/layouts/Sidebar/Sidebar.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import styles from '../../stylesheets/layouts/sidebar/sidebar.module.css';
99
import { Typography, Box, IconButton, Divider } from '@mui/material';
1010
import HomeIcon from '@mui/icons-material/Home';
1111
import AlignHorizontalLeftIcon from '@mui/icons-material/AlignHorizontalLeft';
12-
import RateReviewIcon from '@mui/icons-material/RateReview';
1312
import DashboardIcon from '@mui/icons-material/Dashboard';
1413
// To be uncommented after guest sponsors page is developed
1514
// import VolunteerActivismIcon from '@mui/icons-material/VolunteerActivism';
@@ -37,6 +36,7 @@ import QueryStatsIcon from '@mui/icons-material/QueryStats';
3736
import CurrencyExchangeIcon from '@mui/icons-material/CurrencyExchange';
3837
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
3938
import { useState } from 'react';
39+
import { CalendarIcon } from '@mui/x-date-pickers';
4040

4141
interface SidebarProps {
4242
drawerOpen: boolean;
@@ -101,9 +101,9 @@ const Sidebar = ({ drawerOpen, setDrawerOpen, moveContent, setMoveContent }: Sid
101101
route: routes.CHANGE_REQUESTS
102102
},
103103
{
104-
name: 'Design Review',
105-
icon: <RateReviewIcon />,
106-
route: routes.CALENDAR
104+
name: 'Events',
105+
icon: <CalendarIcon />,
106+
route: routes.EVENTS
107107
}
108108
]
109109
},
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { alpha, Box, Card, CardContent, Chip, Stack, Typography, useTheme } from '@mui/material';
2+
import { EventInstance, formatEventDate, formatEventTime } from 'shared';
3+
import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined';
4+
5+
interface GuestEventCardProps {
6+
event: EventInstance;
7+
}
8+
9+
const GuestEventCard: React.FC<GuestEventCardProps> = ({ event }) => {
10+
const theme = useTheme();
11+
12+
const displayDate = new Date(event.startTime);
13+
const formattedDate = formatEventDate(displayDate);
14+
const formattedTime = formatEventTime(displayDate);
15+
16+
const wbsLabels = event.workPackages.map(
17+
(wp) =>
18+
`${wp.wbsElement.carNumber}.${wp.wbsElement.projectNumber}.${wp.wbsElement.workPackageNumber} - ${wp.wbsElement.name}`
19+
);
20+
21+
const title = wbsLabels.length > 0 ? wbsLabels[0] : event.title;
22+
const extraWbs = wbsLabels.slice(1);
23+
24+
return (
25+
<Card
26+
variant="outlined"
27+
sx={{
28+
width: '100%',
29+
background: theme.palette.background.paper,
30+
borderRadius: 2
31+
}}
32+
>
33+
<CardContent>
34+
<Stack gap={1}>
35+
<Box>
36+
<Typography fontWeight="bold" variant="h6" sx={{ wordBreak: 'break-word' }}>
37+
{title}
38+
</Typography>
39+
{extraWbs.map((label) => (
40+
<Typography key={label} variant="body2" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
41+
{label}
42+
</Typography>
43+
))}
44+
</Box>
45+
46+
<Stack direction="row" flexWrap="wrap" gap={1}>
47+
<Stack direction="row" spacing={0.5} alignItems="center">
48+
<ScheduleOutlinedIcon sx={{ fontSize: 16, color: 'text.secondary', flexShrink: 0 }} />
49+
<Typography variant="body2" color="text.secondary" noWrap>
50+
{formattedDate} @ {formattedTime}
51+
</Typography>
52+
</Stack>
53+
</Stack>
54+
55+
{event.teams.length > 0 && (
56+
<Stack direction="row" flexWrap="wrap" gap={0.5}>
57+
{event.teams.map((team) => (
58+
<Chip
59+
key={team.teamId}
60+
label={team.teamName}
61+
size="small"
62+
variant="filled"
63+
sx={{ bgcolor: alpha(theme.palette.primary.main, 0.45), color: theme.palette.primary.light }}
64+
/>
65+
))}
66+
</Stack>
67+
)}
68+
69+
{event.description && (
70+
<Typography variant="body2" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
71+
{event.description}
72+
</Typography>
73+
)}
74+
</Stack>
75+
</CardContent>
76+
</Card>
77+
);
78+
};
79+
80+
export default GuestEventCard;

0 commit comments

Comments
 (0)