Skip to content

Commit 3e28d28

Browse files
feat: [TEO-454, TEO-508] download event RSVP (#119)
* feat: [TEO-454] download event RSVPs * feat: [TEO-454] disable implicit type conversion, add @type decorators in DTOs * feat: [TEO-508] revert @type additions to DTOs and change going from boolean to enum
1 parent b93b6c5 commit 3e28d28

File tree

11 files changed

+135
-28
lines changed

11 files changed

+135
-28
lines changed

backend/src/api/actions-archive/dto/get-actions-history.dto.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
import {
2-
IsDate,
3-
IsEnum,
4-
IsNumber,
5-
IsOptional,
6-
IsString,
7-
} from 'class-validator';
1+
import { IsDate, IsOptional, IsString } from 'class-validator';
82
import { BasePaginationFilterDto } from 'src/infrastructure/base/base-pagination-filter.dto';
93

104
export class GetManyActionsArchiveDto extends BasePaginationFilterDto {

backend/src/api/event/dto/get-paginated-event-rsvp.dto.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Transform } from 'class-transformer';
2-
import { IsBoolean, IsOptional, IsUUID } from 'class-validator';
1+
import { IsEnum, IsOptional, IsUUID } from 'class-validator';
2+
import { RSVPGoingEnum } from 'src/modules/event/enums/rsvp-going.enum';
33
import { BasePaginationFilterDto } from 'src/infrastructure/base/base-pagination-filter.dto';
44

55
export class GetPaginatedEventRSVPsDto extends BasePaginationFilterDto {
@@ -16,7 +16,6 @@ export class GetPaginatedEventRSVPsDto extends BasePaginationFilterDto {
1616
roleId?: string;
1717

1818
@IsOptional()
19-
@IsBoolean()
20-
@Transform(({ value }) => value === 'true')
21-
going?: boolean;
19+
@IsEnum(RSVPGoingEnum)
20+
going?: RSVPGoingEnum;
2221
}

backend/src/api/event/event.controller.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ import { EventListItemPresenter } from './presenters/event-list-item.presenter';
4040
import { EventPresenter } from './presenters/event.presenter';
4141
import { Response } from 'express';
4242
import { GetManyForDownloadEventUseCase } from 'src/usecases/event/get-many-for-download-event.usecase';
43+
import { GetManyForDownloadEventRSVPUseCase } from 'src/usecases/event/RSVP/get-many-for-download-rsvp.usecase';
44+
import { IEventRSVPDownload } from 'src/modules/event/interfaces/event-rsvp-download.interface';
45+
import { RSVPGoingEnum } from 'src/modules/event/enums/rsvp-going.enum';
4346

4447
@ApiBearerAuth()
4548
@UseGuards(WebJwtAuthGuard, EventGuard)
@@ -55,6 +58,7 @@ export class EventController {
5558
private readonly getManyEventRSVPUsecase: GetManyEventRSVPUseCase,
5659
private readonly getManyEventUseCase: GetManyEventUseCase,
5760
private readonly getManyForDownloadEventUseCase: GetManyForDownloadEventUseCase,
61+
private readonly getManyForDownloadEventRSVPUseCase: GetManyForDownloadEventRSVPUseCase,
5862
) {}
5963

6064
@Get()
@@ -80,7 +84,7 @@ export class EventController {
8084
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
8185
)
8286
@Header('Content-Disposition', 'attachment; filename="Evenimente.xlsx"')
83-
async downloadAccessRequests(
87+
async downloadEvents(
8488
@Res({ passthrough: true }) res: Response,
8589
@ExtractUser() user: IAdminUserModel,
8690
@Query() filters: GetManyEventDto,
@@ -170,6 +174,7 @@ export class EventController {
170174
): Promise<PaginatedPresenter<EventRSVPPresenter>> {
171175
const rsvps = await this.getManyEventRSVPUsecase.execute({
172176
...filters,
177+
going: filters.going ? filters.going === RSVPGoingEnum.YES : undefined,
173178
eventId,
174179
});
175180

@@ -178,4 +183,25 @@ export class EventController {
178183
items: rsvps.items.map((rsvp) => new EventRSVPPresenter(rsvp)),
179184
});
180185
}
186+
187+
@ApiParam({ name: 'id', type: 'string' })
188+
@Get(':id/rsvp/download')
189+
@Header(
190+
'Content-Type',
191+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
192+
)
193+
@Header('Content-Disposition', 'attachment; filename="Lista raspunsuri.xlsx"')
194+
async downloadEventRSVPs(
195+
@Param('id', UuidValidationPipe) eventId: string,
196+
@Res({ passthrough: true }) res: Response,
197+
@Query() filters: GetPaginatedEventRSVPsDto,
198+
): Promise<void> {
199+
const data = await this.getManyForDownloadEventRSVPUseCase.execute({
200+
...filters,
201+
going: filters.going ? filters.going === RSVPGoingEnum.YES : undefined,
202+
eventId,
203+
});
204+
205+
res.end(jsonToExcelBuffer<IEventRSVPDownload>(data, 'Lista raspunsuri'));
206+
}
181207
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum RSVPGoingEnum {
2+
YES = 'yes',
3+
NO = 'no',
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface IEventRSVPDownload {
2+
Nume: string;
3+
Raspuns: string;
4+
'Voluntar in organizatie': string;
5+
Mentiune?: string;
6+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface';
3+
import { IEventRSVPDownload } from 'src/modules/event/interfaces/event-rsvp-download.interface';
4+
import { GetManyEventRSVPUseCase } from './get-many-rsvp.usecase';
5+
import { FindManyEventRSVPOptions } from 'src/modules/event/models/event-rsvp.model';
6+
7+
@Injectable()
8+
export class GetManyForDownloadEventRSVPUseCase
9+
implements IUseCaseService<IEventRSVPDownload[]>
10+
{
11+
constructor(
12+
private readonly getManyEventRSVPUseCase: GetManyEventRSVPUseCase,
13+
) {}
14+
15+
public async execute(
16+
findOptions: FindManyEventRSVPOptions,
17+
): Promise<IEventRSVPDownload[]> {
18+
const eventRSVPs = await this.getManyEventRSVPUseCase.execute({
19+
...findOptions,
20+
limit: 0,
21+
page: 0,
22+
});
23+
24+
return eventRSVPs.items.map((rsvp): IEventRSVPDownload => {
25+
return {
26+
Nume: rsvp.user.name,
27+
Raspuns: rsvp.going ? 'Participa' : 'Nu participa',
28+
'Voluntar in organizatie': rsvp.volunteerId ? 'Da' : 'Nu',
29+
Mentiune: rsvp.mention,
30+
};
31+
});
32+
}
33+
}

backend/src/usecases/use-case.module.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import { GetManyAdminUsersUseCase } from './user/get-many-admin-users.usecase';
7878
import { ActionsArchiveModule } from 'src/modules/actions-archive/actions-archive.module';
7979
import { GetManyActionsArchiveUseCase } from './actions-archive/get-many-actions-archive.usecase';
8080
import { GetActivityLogCountersUsecase } from './activity-log/get-activity-log-counters.usecase';
81+
import { GetManyForDownloadEventRSVPUseCase } from './event/RSVP/get-many-for-download-rsvp.usecase';
8182

8283
@Module({
8384
imports: [
@@ -158,13 +159,14 @@ import { GetActivityLogCountersUsecase } from './activity-log/get-activity-log-c
158159
DeleteEventUseCase,
159160
PublishEventUseCase,
160161
ArchiveEventUseCase,
162+
GetManyForDownloadEventUseCase,
163+
GetManyEventUseCase,
161164
// Events RSVP
162165
CreateEventRSVPUseCase,
163166
GetOneEventRSVPUseCase,
164167
DeleteEventRSVPUseCase,
165168
GetManyEventRSVPUseCase,
166-
GetManyEventUseCase,
167-
GetManyForDownloadEventUseCase,
169+
GetManyForDownloadEventRSVPUseCase,
168170
// Activity Log
169171
CreateActivityLogByAdmin,
170172
GetOneActivityLogUsecase,
@@ -241,13 +243,14 @@ import { GetActivityLogCountersUsecase } from './activity-log/get-activity-log-c
241243
DeleteEventUseCase,
242244
PublishEventUseCase,
243245
ArchiveEventUseCase,
246+
GetManyEventUseCase,
247+
GetManyForDownloadEventUseCase,
244248
// Events RSVP
245249
CreateEventRSVPUseCase,
246250
GetOneEventRSVPUseCase,
247251
DeleteEventRSVPUseCase,
248252
GetManyEventRSVPUseCase,
249-
GetManyEventUseCase,
250-
GetManyForDownloadEventUseCase,
253+
GetManyForDownloadEventRSVPUseCase,
251254
// Activity Log
252255
CreateActivityLogByAdmin,
253256
GetOneActivityLogUsecase,

frontend/src/assets/locales/ro/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,8 @@
491491
"EVENT_006": "Doar evenimentele publicate pot fi arhivate"
492492
},
493493
"download_open": "Evenimente active.xlsx",
494-
"download_past": "Evenimente incheiate.xlsx"
494+
"download_past": "Evenimente incheiate.xlsx",
495+
"download_rsvp": "Lista raspunsuri.xlsx"
495496
},
496497
"announcement": {
497498
"title": "Istoric anunturi",
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export enum RsvpEnum {
2-
GOING = 'going',
3-
NOT_GOING = 'not_going',
1+
export enum RSVPGoingEnum {
2+
YES = 'yes',
3+
NO = 'no',
44
}

frontend/src/pages/Event.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import CardBody from '../components/CardBody';
2626
import FormLayout from '../layouts/FormLayout';
2727
import Paragraph from '../components/Paragraph';
2828
import FormReadOnlyElement from '../components/FormReadOnlyElement';
29-
import { formatDateWithTime } from '../common/utils/utils';
29+
import { downloadExcel, formatDateWithTime } from '../common/utils/utils';
3030
import LoadingContent from '../components/LoadingContent';
3131
import EmptyContent from '../components/EmptyContent';
3232
import { useErrorToast, useSuccessToast } from '../hooks/useToast';
@@ -43,7 +43,8 @@ import { IRsvp } from '../common/interfaces/rsvp.interface';
4343
import DataTableFilters from '../components/DataTableFilters';
4444
import OrganizationStructureSelect from '../containers/OrganizationStructureSelect';
4545
import { DivisionType } from '../common/enums/division-type.enum';
46-
import { RsvpEnum } from '../common/enums/rsvp.enum';
46+
import { RSVPGoingEnum } from '../common/enums/rsvp.enum';
47+
import { getEventRSVPsForDownload } from '../services/event/event.api';
4748

4849
enum EventTab {
4950
EVENT = 'event',
@@ -348,6 +349,21 @@ const Event = () => {
348349
setGoing(undefined);
349350
};
350351

352+
const onExportRSVPs = async () => {
353+
const { data: eventRSVPsData } = await getEventRSVPsForDownload(
354+
id as string,
355+
orderByColumn,
356+
orderDirection,
357+
search,
358+
branch?.key,
359+
department?.key,
360+
role?.key,
361+
going?.key,
362+
);
363+
364+
downloadExcel(eventRSVPsData as BlobPart, i18n.t('events:download_rsvp'));
365+
};
366+
351367
return (
352368
<PageLayout>
353369
<PageHeader onBackButtonPress={navigateBack}>{i18n.t('general:view')}</PageHeader>
@@ -433,8 +449,8 @@ const Event = () => {
433449
onChange={(item: SelectItem<string>) => setGoing(item)}
434450
selected={going}
435451
options={[
436-
{ key: RsvpEnum.GOING, value: i18n.t('events:participate') },
437-
{ key: RsvpEnum.NOT_GOING, value: i18n.t('events:not_participate') },
452+
{ key: RSVPGoingEnum.YES, value: i18n.t('events:participate') },
453+
{ key: RSVPGoingEnum.NO, value: i18n.t('events:not_participate') },
438454
]}
439455
/>
440456
</DataTableFilters>
@@ -445,7 +461,7 @@ const Event = () => {
445461
label={i18n.t('general:download_table')}
446462
icon={<ArrowDownTrayIcon className="h-5 w-5 text-cool-gray-600" />}
447463
className="btn-outline-secondary ml-auto"
448-
onClick={() => alert('Not implemented')}
464+
onClick={onExportRSVPs}
449465
/>
450466
</CardHeader>
451467
<CardBody>

frontend/src/services/event/event.api.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { AxiosResponseHeaders } from 'axios';
22
import { EventState } from '../../common/enums/event-state.enum';
33
import { EventStatus } from '../../common/enums/event-status';
4-
import { RsvpEnum } from '../../common/enums/rsvp.enum';
54
import { OrderDirection } from '../../common/enums/order-direction.enum';
65
import { IEvent } from '../../common/interfaces/event.interface';
76
import { IPaginatedEntity } from '../../common/interfaces/paginated-entity.interface';
@@ -83,11 +82,37 @@ export const getRsvps = async (
8382
branchId,
8483
departmentId,
8584
roleId,
86-
going: going === undefined ? going : going === RsvpEnum.GOING,
85+
going,
8786
},
8887
}).then((res) => res.data);
8988
};
9089

90+
export const getEventRSVPsForDownload = async (
91+
id: string,
92+
orderBy?: string,
93+
orderDirection?: OrderDirection,
94+
search?: string,
95+
branchId?: string,
96+
departmentId?: string,
97+
roleId?: string,
98+
going?: string,
99+
): Promise<{ data: unknown; headers: AxiosResponseHeaders }> => {
100+
return API.get(`event/${id}/rsvp/download`, {
101+
params: {
102+
orderBy,
103+
orderDirection,
104+
search,
105+
branchId,
106+
departmentId,
107+
roleId,
108+
going,
109+
},
110+
responseType: 'arraybuffer',
111+
}).then((res) => {
112+
return { data: res.data, headers: res.headers as AxiosResponseHeaders };
113+
});
114+
};
115+
91116
const formatAddEventPayload = (data: EventFormTypes): object => {
92117
const { targets, tasks, targetType, ...payload } = data;
93118
return {

0 commit comments

Comments
 (0)