diff --git a/backend/src/api/actions-archive/dto/get-actions-history.dto.ts b/backend/src/api/actions-archive/dto/get-actions-history.dto.ts index cdd8395d..dbd5249a 100644 --- a/backend/src/api/actions-archive/dto/get-actions-history.dto.ts +++ b/backend/src/api/actions-archive/dto/get-actions-history.dto.ts @@ -1,10 +1,4 @@ -import { - IsDate, - IsEnum, - IsNumber, - IsOptional, - IsString, -} from 'class-validator'; +import { IsDate, IsOptional, IsString } from 'class-validator'; import { BasePaginationFilterDto } from 'src/infrastructure/base/base-pagination-filter.dto'; export class GetManyActionsArchiveDto extends BasePaginationFilterDto { diff --git a/backend/src/api/event/dto/get-paginated-event-rsvp.dto.ts b/backend/src/api/event/dto/get-paginated-event-rsvp.dto.ts index c07efeb5..1b0d9edd 100644 --- a/backend/src/api/event/dto/get-paginated-event-rsvp.dto.ts +++ b/backend/src/api/event/dto/get-paginated-event-rsvp.dto.ts @@ -1,5 +1,5 @@ -import { Transform } from 'class-transformer'; -import { IsBoolean, IsOptional, IsUUID } from 'class-validator'; +import { IsEnum, IsOptional, IsUUID } from 'class-validator'; +import { RSVPGoingEnum } from 'src/modules/event/enums/rsvp-going.enum'; import { BasePaginationFilterDto } from 'src/infrastructure/base/base-pagination-filter.dto'; export class GetPaginatedEventRSVPsDto extends BasePaginationFilterDto { @@ -16,7 +16,6 @@ export class GetPaginatedEventRSVPsDto extends BasePaginationFilterDto { roleId?: string; @IsOptional() - @IsBoolean() - @Transform(({ value }) => value === 'true') - going?: boolean; + @IsEnum(RSVPGoingEnum) + going?: RSVPGoingEnum; } diff --git a/backend/src/api/event/event.controller.ts b/backend/src/api/event/event.controller.ts index 3cae532f..3e21eb4a 100644 --- a/backend/src/api/event/event.controller.ts +++ b/backend/src/api/event/event.controller.ts @@ -40,6 +40,9 @@ import { EventListItemPresenter } from './presenters/event-list-item.presenter'; import { EventPresenter } from './presenters/event.presenter'; import { Response } from 'express'; import { GetManyForDownloadEventUseCase } from 'src/usecases/event/get-many-for-download-event.usecase'; +import { GetManyForDownloadEventRSVPUseCase } from 'src/usecases/event/RSVP/get-many-for-download-rsvp.usecase'; +import { IEventRSVPDownload } from 'src/modules/event/interfaces/event-rsvp-download.interface'; +import { RSVPGoingEnum } from 'src/modules/event/enums/rsvp-going.enum'; @ApiBearerAuth() @UseGuards(WebJwtAuthGuard, EventGuard) @@ -55,6 +58,7 @@ export class EventController { private readonly getManyEventRSVPUsecase: GetManyEventRSVPUseCase, private readonly getManyEventUseCase: GetManyEventUseCase, private readonly getManyForDownloadEventUseCase: GetManyForDownloadEventUseCase, + private readonly getManyForDownloadEventRSVPUseCase: GetManyForDownloadEventRSVPUseCase, ) {} @Get() @@ -80,7 +84,7 @@ export class EventController { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ) @Header('Content-Disposition', 'attachment; filename="Evenimente.xlsx"') - async downloadAccessRequests( + async downloadEvents( @Res({ passthrough: true }) res: Response, @ExtractUser() user: IAdminUserModel, @Query() filters: GetManyEventDto, @@ -170,6 +174,7 @@ export class EventController { ): Promise> { const rsvps = await this.getManyEventRSVPUsecase.execute({ ...filters, + going: filters.going ? filters.going === RSVPGoingEnum.YES : undefined, eventId, }); @@ -178,4 +183,25 @@ export class EventController { items: rsvps.items.map((rsvp) => new EventRSVPPresenter(rsvp)), }); } + + @ApiParam({ name: 'id', type: 'string' }) + @Get(':id/rsvp/download') + @Header( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ) + @Header('Content-Disposition', 'attachment; filename="Lista raspunsuri.xlsx"') + async downloadEventRSVPs( + @Param('id', UuidValidationPipe) eventId: string, + @Res({ passthrough: true }) res: Response, + @Query() filters: GetPaginatedEventRSVPsDto, + ): Promise { + const data = await this.getManyForDownloadEventRSVPUseCase.execute({ + ...filters, + going: filters.going ? filters.going === RSVPGoingEnum.YES : undefined, + eventId, + }); + + res.end(jsonToExcelBuffer(data, 'Lista raspunsuri')); + } } diff --git a/backend/src/modules/event/enums/rsvp-going.enum.ts b/backend/src/modules/event/enums/rsvp-going.enum.ts new file mode 100644 index 00000000..9792e8c7 --- /dev/null +++ b/backend/src/modules/event/enums/rsvp-going.enum.ts @@ -0,0 +1,4 @@ +export enum RSVPGoingEnum { + YES = 'yes', + NO = 'no', +} diff --git a/backend/src/modules/event/interfaces/event-rsvp-download.interface.ts b/backend/src/modules/event/interfaces/event-rsvp-download.interface.ts new file mode 100644 index 00000000..b7fe4c2f --- /dev/null +++ b/backend/src/modules/event/interfaces/event-rsvp-download.interface.ts @@ -0,0 +1,6 @@ +export interface IEventRSVPDownload { + Nume: string; + Raspuns: string; + 'Voluntar in organizatie': string; + Mentiune?: string; +} diff --git a/backend/src/usecases/event/RSVP/get-many-for-download-rsvp.usecase.ts b/backend/src/usecases/event/RSVP/get-many-for-download-rsvp.usecase.ts new file mode 100644 index 00000000..6bd3403f --- /dev/null +++ b/backend/src/usecases/event/RSVP/get-many-for-download-rsvp.usecase.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface'; +import { IEventRSVPDownload } from 'src/modules/event/interfaces/event-rsvp-download.interface'; +import { GetManyEventRSVPUseCase } from './get-many-rsvp.usecase'; +import { FindManyEventRSVPOptions } from 'src/modules/event/models/event-rsvp.model'; + +@Injectable() +export class GetManyForDownloadEventRSVPUseCase + implements IUseCaseService +{ + constructor( + private readonly getManyEventRSVPUseCase: GetManyEventRSVPUseCase, + ) {} + + public async execute( + findOptions: FindManyEventRSVPOptions, + ): Promise { + const eventRSVPs = await this.getManyEventRSVPUseCase.execute({ + ...findOptions, + limit: 0, + page: 0, + }); + + return eventRSVPs.items.map((rsvp): IEventRSVPDownload => { + return { + Nume: rsvp.user.name, + Raspuns: rsvp.going ? 'Participa' : 'Nu participa', + 'Voluntar in organizatie': rsvp.volunteerId ? 'Da' : 'Nu', + Mentiune: rsvp.mention, + }; + }); + } +} diff --git a/backend/src/usecases/use-case.module.ts b/backend/src/usecases/use-case.module.ts index 917b2449..94a83820 100644 --- a/backend/src/usecases/use-case.module.ts +++ b/backend/src/usecases/use-case.module.ts @@ -78,6 +78,7 @@ import { GetManyAdminUsersUseCase } from './user/get-many-admin-users.usecase'; import { ActionsArchiveModule } from 'src/modules/actions-archive/actions-archive.module'; import { GetManyActionsArchiveUseCase } from './actions-archive/get-many-actions-archive.usecase'; import { GetActivityLogCountersUsecase } from './activity-log/get-activity-log-counters.usecase'; +import { GetManyForDownloadEventRSVPUseCase } from './event/RSVP/get-many-for-download-rsvp.usecase'; @Module({ imports: [ @@ -158,13 +159,14 @@ import { GetActivityLogCountersUsecase } from './activity-log/get-activity-log-c DeleteEventUseCase, PublishEventUseCase, ArchiveEventUseCase, + GetManyForDownloadEventUseCase, + GetManyEventUseCase, // Events RSVP CreateEventRSVPUseCase, GetOneEventRSVPUseCase, DeleteEventRSVPUseCase, GetManyEventRSVPUseCase, - GetManyEventUseCase, - GetManyForDownloadEventUseCase, + GetManyForDownloadEventRSVPUseCase, // Activity Log CreateActivityLogByAdmin, GetOneActivityLogUsecase, @@ -241,13 +243,14 @@ import { GetActivityLogCountersUsecase } from './activity-log/get-activity-log-c DeleteEventUseCase, PublishEventUseCase, ArchiveEventUseCase, + GetManyEventUseCase, + GetManyForDownloadEventUseCase, // Events RSVP CreateEventRSVPUseCase, GetOneEventRSVPUseCase, DeleteEventRSVPUseCase, GetManyEventRSVPUseCase, - GetManyEventUseCase, - GetManyForDownloadEventUseCase, + GetManyForDownloadEventRSVPUseCase, // Activity Log CreateActivityLogByAdmin, GetOneActivityLogUsecase, diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index bfa79fbd..ab41b968 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -491,7 +491,8 @@ "EVENT_006": "Doar evenimentele publicate pot fi arhivate" }, "download_open": "Evenimente active.xlsx", - "download_past": "Evenimente incheiate.xlsx" + "download_past": "Evenimente incheiate.xlsx", + "download_rsvp": "Lista raspunsuri.xlsx" }, "announcement": { "title": "Istoric anunturi", diff --git a/frontend/src/common/enums/rsvp.enum.ts b/frontend/src/common/enums/rsvp.enum.ts index 1356fb51..9792e8c7 100644 --- a/frontend/src/common/enums/rsvp.enum.ts +++ b/frontend/src/common/enums/rsvp.enum.ts @@ -1,4 +1,4 @@ -export enum RsvpEnum { - GOING = 'going', - NOT_GOING = 'not_going', +export enum RSVPGoingEnum { + YES = 'yes', + NO = 'no', } diff --git a/frontend/src/pages/Event.tsx b/frontend/src/pages/Event.tsx index 77749871..d625c0c3 100644 --- a/frontend/src/pages/Event.tsx +++ b/frontend/src/pages/Event.tsx @@ -26,7 +26,7 @@ import CardBody from '../components/CardBody'; import FormLayout from '../layouts/FormLayout'; import Paragraph from '../components/Paragraph'; import FormReadOnlyElement from '../components/FormReadOnlyElement'; -import { formatDateWithTime } from '../common/utils/utils'; +import { downloadExcel, formatDateWithTime } from '../common/utils/utils'; import LoadingContent from '../components/LoadingContent'; import EmptyContent from '../components/EmptyContent'; import { useErrorToast, useSuccessToast } from '../hooks/useToast'; @@ -43,7 +43,8 @@ import { IRsvp } from '../common/interfaces/rsvp.interface'; import DataTableFilters from '../components/DataTableFilters'; import OrganizationStructureSelect from '../containers/OrganizationStructureSelect'; import { DivisionType } from '../common/enums/division-type.enum'; -import { RsvpEnum } from '../common/enums/rsvp.enum'; +import { RSVPGoingEnum } from '../common/enums/rsvp.enum'; +import { getEventRSVPsForDownload } from '../services/event/event.api'; enum EventTab { EVENT = 'event', @@ -348,6 +349,21 @@ const Event = () => { setGoing(undefined); }; + const onExportRSVPs = async () => { + const { data: eventRSVPsData } = await getEventRSVPsForDownload( + id as string, + orderByColumn, + orderDirection, + search, + branch?.key, + department?.key, + role?.key, + going?.key, + ); + + downloadExcel(eventRSVPsData as BlobPart, i18n.t('events:download_rsvp')); + }; + return ( {i18n.t('general:view')} @@ -433,8 +449,8 @@ const Event = () => { onChange={(item: SelectItem) => setGoing(item)} selected={going} options={[ - { key: RsvpEnum.GOING, value: i18n.t('events:participate') }, - { key: RsvpEnum.NOT_GOING, value: i18n.t('events:not_participate') }, + { key: RSVPGoingEnum.YES, value: i18n.t('events:participate') }, + { key: RSVPGoingEnum.NO, value: i18n.t('events:not_participate') }, ]} /> @@ -445,7 +461,7 @@ const Event = () => { label={i18n.t('general:download_table')} icon={} className="btn-outline-secondary ml-auto" - onClick={() => alert('Not implemented')} + onClick={onExportRSVPs} /> diff --git a/frontend/src/services/event/event.api.ts b/frontend/src/services/event/event.api.ts index e808867d..3ee1a1ce 100644 --- a/frontend/src/services/event/event.api.ts +++ b/frontend/src/services/event/event.api.ts @@ -1,7 +1,6 @@ import { AxiosResponseHeaders } from 'axios'; import { EventState } from '../../common/enums/event-state.enum'; import { EventStatus } from '../../common/enums/event-status'; -import { RsvpEnum } from '../../common/enums/rsvp.enum'; import { OrderDirection } from '../../common/enums/order-direction.enum'; import { IEvent } from '../../common/interfaces/event.interface'; import { IPaginatedEntity } from '../../common/interfaces/paginated-entity.interface'; @@ -83,11 +82,37 @@ export const getRsvps = async ( branchId, departmentId, roleId, - going: going === undefined ? going : going === RsvpEnum.GOING, + going, }, }).then((res) => res.data); }; +export const getEventRSVPsForDownload = async ( + id: string, + orderBy?: string, + orderDirection?: OrderDirection, + search?: string, + branchId?: string, + departmentId?: string, + roleId?: string, + going?: string, +): Promise<{ data: unknown; headers: AxiosResponseHeaders }> => { + return API.get(`event/${id}/rsvp/download`, { + params: { + orderBy, + orderDirection, + search, + branchId, + departmentId, + roleId, + going, + }, + responseType: 'arraybuffer', + }).then((res) => { + return { data: res.data, headers: res.headers as AxiosResponseHeaders }; + }); +}; + const formatAddEventPayload = (data: EventFormTypes): object => { const { targets, tasks, targetType, ...payload } = data; return {