diff --git a/packages/event/data-access/src/lib/application/event.facade.ts b/packages/event/data-access/src/lib/application/event.facade.ts index 2086204a..f7043b6a 100644 --- a/packages/event/data-access/src/lib/application/event.facade.ts +++ b/packages/event/data-access/src/lib/application/event.facade.ts @@ -12,6 +12,7 @@ import { CopyEventUseCase, FindMyEventsUseCase, FindEventsUntilUseCase, + FindEventsDateRangeUseCase, } from '@devmx/event-domain/client'; export class EventFacade extends EntityFacade { @@ -28,6 +29,7 @@ export class EventFacade extends EntityFacade { private findMyEventsUseCase: FindMyEventsUseCase, private findAllEventsUseCase: FindAllEventsUseCase, private findEventsUntilUseCase: FindEventsUntilUseCase, + private findEventsDateRangeUseCase: FindEventsDateRangeUseCase, private findEventByIDUseCase: FindEventByIDUseCase, private updateEventUseCase: UpdateEventUseCase, private copyEventUseCase: CopyEventUseCase, @@ -53,6 +55,11 @@ export class EventFacade extends EntityFacade { this.onLoad(this.findAllEventsUseCase.execute(this.state.params)); } + loadDateRange(start: Date, end: Date) { + const params = { start, end, ...this.state.params }; + this.onLoad(this.findEventsDateRangeUseCase.execute(params)); + } + loadUntil() { this.onLoad(this.findEventsUntilUseCase.execute(this.state.params)); } @@ -101,6 +108,7 @@ export function provideEventFacade() { FindMyEventsUseCase, FindAllEventsUseCase, FindEventsUntilUseCase, + FindEventsDateRangeUseCase, FindEventByIDUseCase, UpdateEventUseCase, CopyEventUseCase, diff --git a/packages/event/data-access/src/lib/infrastructure/event.http.service.impl.ts b/packages/event/data-access/src/lib/infrastructure/event.http.service.impl.ts index 256a5e80..ed6096af 100644 --- a/packages/event/data-access/src/lib/infrastructure/event.http.service.impl.ts +++ b/packages/event/data-access/src/lib/infrastructure/event.http.service.impl.ts @@ -25,6 +25,14 @@ export class EventHttpServiceImpl return this.http.get>(url.join('?')); } + findDateRange(start: Date, end: Date, params: QueryParams) { + const url = [ + `${this.url}/range/${start}/${end}`, + createQueryParams(params), + ]; + return this.http.get>(url.join('?')); + } + findMyEvents(params: QueryParams) { const url = [`${this.url}/my`, createQueryParams(params)]; return this.http.get>(url.join('?')); diff --git a/packages/event/data-access/src/lib/providers/event.ts b/packages/event/data-access/src/lib/providers/event.ts index bc34ed91..57dc77e3 100644 --- a/packages/event/data-access/src/lib/providers/event.ts +++ b/packages/event/data-access/src/lib/providers/event.ts @@ -6,6 +6,7 @@ import { provideDeleteEventUseCase, provideFindAllEventsUseCase, provideFindEventByIDUseCase, + provideFindEventsDateRangeUseCase, provideFindEventsUntilUseCase, provideFindEventsUseCase, provideFindMyEventsUseCase, @@ -21,6 +22,7 @@ export function provideEvent() { provideFindMyEventsUseCase(), provideFindAllEventsUseCase(), provideFindEventsUntilUseCase(), + provideFindEventsDateRangeUseCase(), provideFindEventByIDUseCase(), provideUpdateEventUseCase(), provideDeleteEventUseCase(), diff --git a/packages/event/data-source/src/lib/application/events.facade.ts b/packages/event/data-source/src/lib/application/events.facade.ts index ef76137f..bf4a89ab 100644 --- a/packages/event/data-source/src/lib/application/events.facade.ts +++ b/packages/event/data-source/src/lib/application/events.facade.ts @@ -4,13 +4,14 @@ import { UpdateEventDto, CopyEventDto, } from '../dtos'; -import { Event } from '@devmx/shared-api-interfaces'; +import { Event, QueryParamsDateRange } from '@devmx/shared-api-interfaces'; import { plainToInstance } from 'class-transformer'; import { CopyEventUseCase, CreateEventUseCase, DeleteEventUseCase, FindEventByIDUseCase, + FindEventsDateRangeUseCase, FindEventsFromUseCase, FindEventsUntilUseCase, FindEventsUseCase, @@ -30,6 +31,7 @@ export class EventsFacade { private findMyEventsUseCase: FindMyEventsUseCase, private findEventsFromUseCase: FindEventsFromUseCase, private findEventsUntilUseCase: FindEventsUntilUseCase, + private findEventsDateRangeUseCase: FindEventsDateRangeUseCase, private findEventByIDUseCase: FindEventByIDUseCase, private updateEventUseCase: UpdateEventUseCase, private copyEventUseCase: CopyEventUseCase, @@ -60,6 +62,13 @@ export class EventsFacade { return new PageDto(events, items, pages); } + async findDateRange(params: QueryParamsDateRange) { + const { data, items, pages } = + await this.findEventsDateRangeUseCase.execute(params); + const events = plainToInstance(EventDto, data); + return new PageDto(events, items, pages); + } + async findFrom(date: Date, params: QueryParamsDto) { const { data, items, pages } = await this.findEventsFromUseCase.execute([ date, @@ -101,6 +110,7 @@ export function provideEventsFacade() { FindMyEventsUseCase, FindEventsFromUseCase, FindEventsUntilUseCase, + FindEventsDateRangeUseCase, FindEventByIDUseCase, UpdateEventUseCase, CopyEventUseCase, diff --git a/packages/event/data-source/src/lib/infrastructure/events.mongo.service.impl.ts b/packages/event/data-source/src/lib/infrastructure/events.mongo.service.impl.ts index cbae1964..d09d04a7 100644 --- a/packages/event/data-source/src/lib/infrastructure/events.mongo.service.impl.ts +++ b/packages/event/data-source/src/lib/infrastructure/events.mongo.service.impl.ts @@ -1,8 +1,12 @@ -import { QueryParams, EditableEntity } from '@devmx/shared-api-interfaces'; import { Query, RootFilterQuery, SortOrder } from 'mongoose'; import { EventsService } from '@devmx/event-domain/server'; import { getModelToken } from '@nestjs/mongoose'; import { EventCollection } from '../schemas'; +import { + QueryParams, + EditableEntity, + QueryParamsDateRange, +} from '@devmx/shared-api-interfaces'; import { objectId, MongoService, @@ -13,28 +17,30 @@ export class EventsMongoServiceImpl extends MongoService implements EventsService { - async findMyEvents(params: QueryParams) { - const { page = 0, size = 10, filter, sort } = params; + async findDateRange(params: QueryParamsDateRange) { + const { page = 0, size = 10, start, end } = params; const skip = page * size; - const where = this.applyFilter(filter ?? {}); - const order = this.applySort(sort ?? {}); + const filter = this.applyFilter(params.filter ?? {}); + const sort = this.applySort(params.sort ?? {}); - const { owner = '' } = filter ?? {}; + const where = { ...filter, date: { $gte: start, $lte: end } }; - const query = this.entityModel - .find({ leaders: { $in: [objectId(String(owner))] } }) - .sort(order) - .skip(skip) - .limit(size); + return this.findByWhere(where, sort, skip, size); + } - const entities = await this.applyPopulate(query).exec(); + async findMyEvents(params: QueryParams) { + const { page = 0, size = 10 } = params; - const data = entities.map((item) => item.toJSON()); - const items = await this.entityModel.countDocuments(where).exec(); - const pages = Math.ceil(items / size); + const skip = page * size; + const filter = this.applyFilter(params.filter ?? {}); + const sort = this.applySort(params.sort ?? {}); - return { data, items, pages }; + const { owner = '' } = params.filter ?? {}; + + const where = { ...filter, leaders: { $in: [objectId(String(owner))] } }; + + return this.findByWhere(where, sort, skip, size); } async findFrom(date: Date, params: QueryParams) { diff --git a/packages/event/data-source/src/lib/providers/event.ts b/packages/event/data-source/src/lib/providers/event.ts index 84550bfc..4d97c5a5 100644 --- a/packages/event/data-source/src/lib/providers/event.ts +++ b/packages/event/data-source/src/lib/providers/event.ts @@ -10,6 +10,7 @@ import { provideCopyEventUseCase, provideFindMyEventsUseCase, provideFindEventsUntilUseCase, + provideFindEventsDateRangeUseCase, } from '@devmx/event-domain/server'; export function provideEvent() { @@ -21,6 +22,7 @@ export function provideEvent() { provideFindMyEventsUseCase(), provideFindEventsFromUseCase(), provideFindEventsUntilUseCase(), + provideFindEventsDateRangeUseCase(), provideFindEventByIDUseCase(), provideUpdateEventUseCase(), provideDeleteEventUseCase(), diff --git a/packages/event/domain/src/client/services/event.ts b/packages/event/domain/src/client/services/event.ts index 1e7c4435..02ef89a2 100644 --- a/packages/event/domain/src/client/services/event.ts +++ b/packages/event/domain/src/client/services/event.ts @@ -15,6 +15,8 @@ export abstract class EventService extends EntityService { abstract findAll(params: QueryParams): Observable>; abstract findUntil(params: QueryParams): Observable>; + // prettier-ignore + abstract findDateRange(start: Date, end: Date, params: QueryParams): Observable>; abstract findMyEvents(params: QueryParams): Observable>; diff --git a/packages/event/domain/src/client/use-cases/find-events-date-range.ts b/packages/event/domain/src/client/use-cases/find-events-date-range.ts new file mode 100644 index 00000000..a19e115f --- /dev/null +++ b/packages/event/domain/src/client/use-cases/find-events-date-range.ts @@ -0,0 +1,23 @@ +import { createUseCaseProvider } from '@devmx/shared-util-data/client'; +import { EventService } from '../services'; +import { + Page, + Event, + UseCase, + QueryParams, + QueryParamsDateRange, +} from '@devmx/shared-api-interfaces'; + +export class FindEventsDateRangeUseCase + implements UseCase, Page> +{ + constructor(private eventService: EventService) {} + + execute({ start, end, ...params }: QueryParamsDateRange) { + return this.eventService.findDateRange(start, end, params); + } +} + +export function provideFindEventsDateRangeUseCase() { + return createUseCaseProvider(FindEventsDateRangeUseCase, [EventService]); +} diff --git a/packages/event/domain/src/client/use-cases/index.ts b/packages/event/domain/src/client/use-cases/index.ts index b6d25730..393db5fc 100644 --- a/packages/event/domain/src/client/use-cases/index.ts +++ b/packages/event/domain/src/client/use-cases/index.ts @@ -4,6 +4,7 @@ export * from './create-rsvp'; export * from './delete-event'; export * from './find-all-events'; export * from './find-event-by-id'; +export * from './find-events-date-range'; export * from './find-events-until'; export * from './find-events'; export * from './find-my-events'; diff --git a/packages/event/domain/src/server/services/events.ts b/packages/event/domain/src/server/services/events.ts index 7cbe1344..fe38d0e1 100644 --- a/packages/event/domain/src/server/services/events.ts +++ b/packages/event/domain/src/server/services/events.ts @@ -1,5 +1,10 @@ import { EntityService } from '@devmx/shared-api-interfaces/server'; -import { Event, Page, QueryParams } from '@devmx/shared-api-interfaces'; +import { + Event, + Page, + QueryParams, + QueryParamsDateRange, +} from '@devmx/shared-api-interfaces'; export abstract class EventsService extends EntityService { abstract findFrom( @@ -12,5 +17,9 @@ export abstract class EventsService extends EntityService { params: QueryParams ): Promise>; + abstract findDateRange( + params: QueryParamsDateRange + ): Promise>; + abstract findMyEvents(params: QueryParams): Promise>; } diff --git a/packages/event/domain/src/server/use-cases/find-events-date-range.ts b/packages/event/domain/src/server/use-cases/find-events-date-range.ts new file mode 100644 index 00000000..60143539 --- /dev/null +++ b/packages/event/domain/src/server/use-cases/find-events-date-range.ts @@ -0,0 +1,48 @@ +import { createUseCaseProvider } from '@devmx/shared-util-data/server'; +import { EventsService } from '../services'; +import { + Page, + Event, + UseCase, + QueryParamsDateRange, +} from '@devmx/shared-api-interfaces'; + +export class FindEventsDateRangeUseCase + implements UseCase, Page> +{ + constructor(private eventsService: EventsService) {} + + async execute(params: QueryParamsDateRange) { + if (params.filter) { + if (params.filter.format) { + params.filter.format = new RegExp(params.filter.format, 'i'); + } else { + delete params.filter.format; + } + + if (params.filter.title) { + params.filter.title = new RegExp(params.filter.title, 'i'); + } else { + delete params.filter.title; + } + + if (params.filter.city) { + params.filter.city = params.filter.city.toString(); + } else { + delete params.filter.city; + } + + if (params.filter.description) { + params.filter.description = new RegExp(params.filter.description, 'i'); + } else { + delete params.filter.description; + } + } + + return await this.eventsService.findDateRange(params); + } +} + +export function provideFindEventsDateRangeUseCase() { + return createUseCaseProvider(FindEventsDateRangeUseCase, [EventsService]); +} diff --git a/packages/event/domain/src/server/use-cases/index.ts b/packages/event/domain/src/server/use-cases/index.ts index da75ddd7..12b3564f 100644 --- a/packages/event/domain/src/server/use-cases/index.ts +++ b/packages/event/domain/src/server/use-cases/index.ts @@ -3,6 +3,7 @@ export * from './create-event'; export * from './create-rsvp'; export * from './delete-event'; export * from './find-event-by-id'; +export * from './find-events-date-range'; export * from './find-events-from'; export * from './find-events-until'; export * from './find-events'; diff --git a/packages/event/feature-shell/src/lib/containers/events/events.container.html b/packages/event/feature-shell/src/lib/containers/events/events.container.html index 017f863e..31519fb2 100644 --- a/packages/event/feature-shell/src/lib/containers/events/events.container.html +++ b/packages/event/feature-shell/src/lib/containers/events/events.container.html @@ -1,11 +1,15 @@
-
+
-
+ - +
+ + + +
@defer (on timer(500ms)) { diff --git a/packages/event/feature-shell/src/lib/containers/events/events.container.scss b/packages/event/feature-shell/src/lib/containers/events/events.container.scss index 29ccbece..b71ab8d2 100644 --- a/packages/event/feature-shell/src/lib/containers/events/events.container.scss +++ b/packages/event/feature-shell/src/lib/containers/events/events.container.scss @@ -8,6 +8,12 @@ flex-flow: row wrap; justify-content: space-between; padding: 0.4em 0.8em; + + div { + gap: 1em; + display: flex; + align-items: baseline; + } } .events-container { diff --git a/packages/event/feature-shell/src/lib/containers/events/events.container.ts b/packages/event/feature-shell/src/lib/containers/events/events.container.ts index 56d3d6e5..18a3f304 100644 --- a/packages/event/feature-shell/src/lib/containers/events/events.container.ts +++ b/packages/event/feature-shell/src/lib/containers/events/events.container.ts @@ -7,7 +7,9 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EventFacade } from '@devmx/event-data-access'; import { AsyncPipe } from '@angular/common'; import { + DateRange, EventCardComponent, + EventDateRangeComponent, EventFilterComponent, EventTimeComponent, } from '@devmx/event-ui-shared'; @@ -21,6 +23,7 @@ import { PaginatorComponent, EventTimeComponent, EventFilterComponent, + EventDateRangeComponent, SortDirectionComponent, EventCardComponent, SkeletonComponent, @@ -48,11 +51,15 @@ export class EventsContainer { const filter = { title, format }; + const { start, end } = params; + const sort = { date }; this.eventFacade.setParams({ page, size, filter, sort }); - if (time === 'until') { + if (start && end) { + this.eventFacade.loadDateRange(new Date(start), new Date(end)); + } else if (time === 'until') { this.eventFacade.loadUntil(); } else { this.eventFacade.load(); @@ -60,7 +67,7 @@ export class EventsContainer { }; onFilterChange(format: string) { - const queryParams = { format }; + const queryParams = this.mergeQueryParams({ format }); this.router.navigate([], { queryParams }); } @@ -69,12 +76,21 @@ export class EventsContainer { this.router.navigate([], { queryParams }); } + onRangeChange({ start, end }: DateRange) { + const queryParams = this.mergeQueryParams({ start, end }); + this.router.navigate([], { queryParams }); + } + onSortChange(date: string) { - const queryParams = { date }; + const queryParams = this.mergeQueryParams({ date }); this.router.navigate([], { queryParams }); } onPageChange(queryParams: PageParams) { this.router.navigate([], { queryParams }); } + + mergeQueryParams(queryParams: object) { + return { ...this.route.snapshot.queryParams, ...queryParams }; + } } diff --git a/packages/event/resource/src/lib/controllers/events.ts b/packages/event/resource/src/lib/controllers/events.ts index 00f8072b..2a87e033 100644 --- a/packages/event/resource/src/lib/controllers/events.ts +++ b/packages/event/resource/src/lib/controllers/events.ts @@ -1,6 +1,8 @@ import { ApiTags, ApiOkResponse, ApiBearerAuth } from '@nestjs/swagger'; import { AuthUser, Event } from '@devmx/shared-api-interfaces'; -import { exceptionByError } from '@devmx/shared-resource'; +import { exceptionByError, ParseDatePipe } from '@devmx/shared-resource'; +import { subDays } from 'date-fns/subDays'; +import { Response } from 'express'; import { User, Roles, @@ -26,17 +28,15 @@ import { import { EventDto, RSVPDto, + RSVPsFacade, EventsFacade, CreateEventDto, UpdateEventDto, - RSVPsFacade, CreateRSVPDto, - createSitemapFromEvents, CopyEventDto, + createSitemapFromEvents, } from '@devmx/event-data-source'; import 'multer'; -import { subDays } from 'date-fns/subDays'; -import { Response } from 'express'; @ApiTags('Eventos') @Controller('events') @@ -93,6 +93,21 @@ export class EventsController { } } + @Get('range/:start/:end') + @Allowed() + @ApiPage(EventDto) + async findRange( + @Param('start', new ParseDatePipe()) start: Date, + @Param('end', new ParseDatePipe()) end: Date, + @Query() params: QueryParamsDto + ) { + try { + return await this.eventsFacade.findDateRange({ ...params, start, end }); + } catch (err) { + throw new BadRequestException(err); + } + } + @Get('until') @Allowed() @ApiPage(EventDto) diff --git a/packages/event/ui-shared/src/lib/components/event-date-range/event-date-range.component.html b/packages/event/ui-shared/src/lib/components/event-date-range/event-date-range.component.html new file mode 100644 index 00000000..e44e7ed0 --- /dev/null +++ b/packages/event/ui-shared/src/lib/components/event-date-range/event-date-range.component.html @@ -0,0 +1,13 @@ + + + + + + + + + @if (form.controls.start.hasError('matStartDateInvalid')) { Data inicial } + @if (form.controls.end.hasError('matEndDateInvalid')) { Data final } + inválida + + diff --git a/packages/event/ui-shared/src/lib/components/event-date-range/event-date-range.component.scss b/packages/event/ui-shared/src/lib/components/event-date-range/event-date-range.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/packages/event/ui-shared/src/lib/components/event-date-range/event-date-range.component.ts b/packages/event/ui-shared/src/lib/components/event-date-range/event-date-range.component.ts new file mode 100644 index 00000000..6056b1e0 --- /dev/null +++ b/packages/event/ui-shared/src/lib/components/event-date-range/event-date-range.component.ts @@ -0,0 +1,29 @@ +import { ChangeDetectionStrategy, Component, output } from '@angular/core'; +import { DateRange, EventDateRangeForm } from './event-date-range'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { provideNativeDateAdapter } from '@angular/material/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ReactiveFormsModule } from '@angular/forms'; + +@Component({ + selector: 'devmx-event-date-range', + templateUrl: 'event-date-range.component.html', + styleUrl: 'event-date-range.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [provideNativeDateAdapter()], + imports: [ReactiveFormsModule, MatFormFieldModule, MatDatepickerModule], +}) +export class EventDateRangeComponent { + rangeChange = output(); + + readonly form = new EventDateRangeForm(); + + constructor() { + this.form.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => { + if (value.start instanceof Date && value.end instanceof Date) { + this.rangeChange.emit(this.form.getRawValue()); + } + }); + } +} diff --git a/packages/event/ui-shared/src/lib/components/event-date-range/event-date-range.ts b/packages/event/ui-shared/src/lib/components/event-date-range/event-date-range.ts new file mode 100644 index 00000000..4780fb6a --- /dev/null +++ b/packages/event/ui-shared/src/lib/components/event-date-range/event-date-range.ts @@ -0,0 +1,16 @@ +import { TypedForm } from '@devmx/shared-ui-global/forms'; +import { FormControl, FormGroup } from '@angular/forms'; + +export interface DateRange { + start: Date; + end: Date; +} + +export class EventDateRangeForm extends FormGroup> { + constructor() { + super({ + start: new FormControl(), + end: new FormControl(), + }); + } +} diff --git a/packages/event/ui-shared/src/lib/components/index.ts b/packages/event/ui-shared/src/lib/components/index.ts index fde04580..1cb294f4 100644 --- a/packages/event/ui-shared/src/lib/components/index.ts +++ b/packages/event/ui-shared/src/lib/components/index.ts @@ -1,3 +1,5 @@ +export * from './event-date-range/event-date-range.component'; +export * from './event-date-range/event-date-range'; export * from './event-card-list/event-card-list.component'; export * from './event-filter/event-filter.component'; export * from './rsvp-button/rsvp-button.component'; diff --git a/packages/shared/api-interfaces/src/lib/interfaces/index.ts b/packages/shared/api-interfaces/src/lib/interfaces/index.ts index 83b5414d..d0cfc0fd 100644 --- a/packages/shared/api-interfaces/src/lib/interfaces/index.ts +++ b/packages/shared/api-interfaces/src/lib/interfaces/index.ts @@ -16,6 +16,7 @@ export * from './location'; export * from './name'; export * from './page'; export * from './query-location'; +export * from './query-params-date-range'; export * from './query-params'; export * from './range'; export * from './response-message'; diff --git a/packages/shared/api-interfaces/src/lib/interfaces/query-params-date-range.ts b/packages/shared/api-interfaces/src/lib/interfaces/query-params-date-range.ts new file mode 100644 index 00000000..d67db656 --- /dev/null +++ b/packages/shared/api-interfaces/src/lib/interfaces/query-params-date-range.ts @@ -0,0 +1,7 @@ +import { QueryParams } from './query-params'; + +export interface QueryParamsDateRange extends QueryParams { + start: Date; + + end: Date; +} diff --git a/packages/shared/resource/src/index.ts b/packages/shared/resource/src/index.ts index ba312fb9..f04fe037 100644 --- a/packages/shared/resource/src/index.ts +++ b/packages/shared/resource/src/index.ts @@ -3,4 +3,5 @@ export * from './lib/shared-database.module'; export * from './lib/shared-github.module'; export * from './lib/shared-mailer.module'; export * from './lib/guards'; +export * from './lib/pipes'; export * from './lib/utils'; diff --git a/packages/shared/resource/src/lib/pipes/index.ts b/packages/shared/resource/src/lib/pipes/index.ts new file mode 100644 index 00000000..9d59dbb0 --- /dev/null +++ b/packages/shared/resource/src/lib/pipes/index.ts @@ -0,0 +1 @@ +export * from './parse-date'; diff --git a/packages/shared/resource/src/lib/pipes/parse-date.ts b/packages/shared/resource/src/lib/pipes/parse-date.ts new file mode 100644 index 00000000..8b8fec48 --- /dev/null +++ b/packages/shared/resource/src/lib/pipes/parse-date.ts @@ -0,0 +1,23 @@ +import { + Injectable, + PipeTransform, + ArgumentMetadata, + BadRequestException, +} from '@nestjs/common'; + +@Injectable() +export class ParseDatePipe implements PipeTransform { + transform(value: string, metadata: ArgumentMetadata) { + const date = new Date(value); + + if (!this.validate(date)) { + throw new BadRequestException(`${metadata.data} não é uma data válida`); + } + + return date; + } + + validate(date: Date) { + return date instanceof Date && !isNaN(date.getTime()); + } +}