From 016e7ad91fff67ae4f07f879ec0fb2b8c992a1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kope=C4=8Dek?= Date: Tue, 6 Feb 2024 22:15:33 +0000 Subject: [PATCH] feat: wip: event view --- .../event-accounting.component.html | 29 +++ .../event-accounting.component.scss} | 0 .../event-accounting.component.ts} | 24 +-- .../event-attendees.component.html | 31 ++++ .../event-attendees.component.scss} | 0 .../event-attendees.component.ts | 119 +++++++++++++ .../event-expenses-chart.component.ts | 20 ++- .../event-info/event-info.component.html | 35 ++++ .../event-info/event-info.component.scss} | 4 + .../event-info/event-info.component.ts | 75 ++++++++ .../event-registration.component.html | 34 ++++ .../event-registration.component.scss} | 0 .../event-registration.component.ts} | 22 +-- .../event-report/event-report.component.html | 1 + .../event-report/event-report.component.scss} | 0 .../event-report/event-report.component.ts | 13 ++ .../event-type-selector.component.html} | 0 .../event-type-selector.component.scss} | 2 +- .../event-type-selector.component.ts} | 14 +- .../member-selector-modal.component.html | 8 +- .../member-selector-modal.component.ts | 20 ++- .../modules/events/events-routing.module.ts | 26 +-- .../app/modules/events/events.component.html | 33 ---- .../app/modules/events/events.component.scss | 29 --- .../app/modules/events/events.component.ts | 43 ----- .../src/app/modules/events/events.module.ts | 36 ++-- .../event-view/event-view.component.html | 118 +++++++++++++ .../event-view/event-view.component.scss | 0 .../pages/event-view/event-view.component.ts | 167 ++++++++++++++++++ .../events-view-accounting.component.html | 41 ----- .../events-view-attendees.component.html | 37 ---- .../events-view-attendees.component.ts | 139 --------------- .../events-view-info.component.html | 42 ----- .../events-view-info.component.ts | 166 ----------------- .../events-view-registration.component.html | 33 ---- .../events-view-report.component.html | 13 -- .../events-view-report.component.ts | 23 --- .../modules/events/schema/event-actions.ts | 9 - .../members-view/members-view.component.html | 3 +- frontend/src/app/services/modal.service.ts | 41 ++++- .../edit-button/edit-button.component.html | 2 +- .../edit-button/edit-button.component.ts | 1 + .../components/item/item.component.html | 8 + .../components/item/item.component.scss | 4 + .../shared/components/item/item.component.ts | 15 ++ .../shared/components/tab/tab.component.html | 5 + .../shared/components/tab/tab.component.scss | 0 .../shared/components/tab/tab.component.ts | 41 +++++ .../components/tabs/tabs.component.html | 3 + .../components/tabs/tabs.component.scss | 3 + .../shared/components/tabs/tabs.component.ts | 38 ++++ .../src/app/shared/pipes/date-range.pipe.ts | 41 +++-- frontend/src/app/shared/shared.module.ts | 9 + 53 files changed, 898 insertions(+), 722 deletions(-) create mode 100644 frontend/src/app/modules/events/components/event-accounting/event-accounting.component.html rename frontend/src/app/modules/events/{pages/events-view-accounting/events-view-accounting.component.scss => components/event-accounting/event-accounting.component.scss} (100%) rename frontend/src/app/modules/events/{pages/events-view-accounting/events-view-accounting.component.ts => components/event-accounting/event-accounting.component.ts} (86%) create mode 100644 frontend/src/app/modules/events/components/event-attendees/event-attendees.component.html rename frontend/src/app/modules/events/{pages/events-view-attendees/events-view-attendees.component.scss => components/event-attendees/event-attendees.component.scss} (100%) create mode 100644 frontend/src/app/modules/events/components/event-attendees/event-attendees.component.ts create mode 100644 frontend/src/app/modules/events/components/event-info/event-info.component.html rename frontend/src/app/modules/events/{pages/events-view-info/events-view-info.component.scss => components/event-info/event-info.component.scss} (62%) create mode 100644 frontend/src/app/modules/events/components/event-info/event-info.component.ts create mode 100644 frontend/src/app/modules/events/components/event-registration/event-registration.component.html rename frontend/src/app/modules/events/{pages/events-view-registration/events-view-registration.component.scss => components/event-registration/event-registration.component.scss} (100%) rename frontend/src/app/modules/events/{pages/events-view-registration/events-view-registration.component.ts => components/event-registration/event-registration.component.ts} (83%) create mode 100644 frontend/src/app/modules/events/components/event-report/event-report.component.html rename frontend/src/app/modules/events/{pages/events-view-report/events-view-report.component.scss => components/event-report/event-report.component.scss} (100%) create mode 100644 frontend/src/app/modules/events/components/event-report/event-report.component.ts rename frontend/src/app/modules/events/components/{event-subtype-selector/event-subtype-selector.component.html => event-type-selector/event-type-selector.component.html} (100%) rename frontend/src/app/modules/events/components/{event-subtype-selector/event-subtype-selector.component.scss => event-type-selector/event-type-selector.component.scss} (94%) rename frontend/src/app/modules/events/components/{event-subtype-selector/event-subtype-selector.component.ts => event-type-selector/event-type-selector.component.ts} (80%) delete mode 100644 frontend/src/app/modules/events/events.component.html delete mode 100644 frontend/src/app/modules/events/events.component.scss delete mode 100644 frontend/src/app/modules/events/events.component.ts create mode 100644 frontend/src/app/modules/events/pages/event-view/event-view.component.html create mode 100644 frontend/src/app/modules/events/pages/event-view/event-view.component.scss create mode 100644 frontend/src/app/modules/events/pages/event-view/event-view.component.ts delete mode 100644 frontend/src/app/modules/events/pages/events-view-accounting/events-view-accounting.component.html delete mode 100644 frontend/src/app/modules/events/pages/events-view-attendees/events-view-attendees.component.html delete mode 100644 frontend/src/app/modules/events/pages/events-view-attendees/events-view-attendees.component.ts delete mode 100644 frontend/src/app/modules/events/pages/events-view-info/events-view-info.component.html delete mode 100644 frontend/src/app/modules/events/pages/events-view-info/events-view-info.component.ts delete mode 100644 frontend/src/app/modules/events/pages/events-view-registration/events-view-registration.component.html delete mode 100644 frontend/src/app/modules/events/pages/events-view-report/events-view-report.component.html delete mode 100644 frontend/src/app/modules/events/pages/events-view-report/events-view-report.component.ts delete mode 100644 frontend/src/app/modules/events/schema/event-actions.ts create mode 100644 frontend/src/app/shared/components/item/item.component.html create mode 100644 frontend/src/app/shared/components/item/item.component.scss create mode 100644 frontend/src/app/shared/components/item/item.component.ts create mode 100644 frontend/src/app/shared/components/tab/tab.component.html create mode 100644 frontend/src/app/shared/components/tab/tab.component.scss create mode 100644 frontend/src/app/shared/components/tab/tab.component.ts create mode 100644 frontend/src/app/shared/components/tabs/tabs.component.html create mode 100644 frontend/src/app/shared/components/tabs/tabs.component.scss create mode 100644 frontend/src/app/shared/components/tabs/tabs.component.ts diff --git a/frontend/src/app/modules/events/components/event-accounting/event-accounting.component.html b/frontend/src/app/modules/events/components/event-accounting/event-accounting.component.html new file mode 100644 index 000000000..908031b66 --- /dev/null +++ b/frontend/src/app/modules/events/components/event-accounting/event-accounting.component.html @@ -0,0 +1,29 @@ +

Účtování

+ + + + + +

+ {{ expense.id }} - {{ expense.description }} + +

+

{{ expense.amount | number: "1.2-2" : "cs" }} Kč

+
+ + {{ expense.type }} + + + {{ expense.amount | number: "1.2-2" : "cs" }} Kč + +
+ + + + + + + + +
+
diff --git a/frontend/src/app/modules/events/pages/events-view-accounting/events-view-accounting.component.scss b/frontend/src/app/modules/events/components/event-accounting/event-accounting.component.scss similarity index 100% rename from frontend/src/app/modules/events/pages/events-view-accounting/events-view-accounting.component.scss rename to frontend/src/app/modules/events/components/event-accounting/event-accounting.component.scss diff --git a/frontend/src/app/modules/events/pages/events-view-accounting/events-view-accounting.component.ts b/frontend/src/app/modules/events/components/event-accounting/event-accounting.component.ts similarity index 86% rename from frontend/src/app/modules/events/pages/events-view-accounting/events-view-accounting.component.ts rename to frontend/src/app/modules/events/components/event-accounting/event-accounting.component.ts index ea6d62af5..900c08803 100644 --- a/frontend/src/app/modules/events/pages/events-view-accounting/events-view-accounting.component.ts +++ b/frontend/src/app/modules/events/components/event-accounting/event-accounting.component.ts @@ -1,6 +1,6 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component, Input, OnDestroy, OnInit } from "@angular/core"; import { AlertController, ModalController } from "@ionic/angular"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { UntilDestroy } from "@ngneat/until-destroy"; import { EventExpenseResponse, EventResponseWithLinks } from "src/app/api"; import { EventExpenseTypes } from "src/app/config/event-expense-types"; import { EventExpenseModalComponent } from "src/app/modules/events/components/event-expense-modal/event-expense-modal.component"; @@ -10,12 +10,12 @@ import { Action } from "src/app/shared/components/action-buttons/action-buttons. @UntilDestroy() @Component({ - selector: "bo-events-view-accounting", - templateUrl: "./events-view-accounting.component.html", - styleUrls: ["./events-view-accounting.component.scss"], + selector: "bo-event-accounting", + templateUrl: "./event-accounting.component.html", + styleUrls: ["./event-accounting.component.scss"], }) -export class EventsViewAccountingComponent implements OnInit, OnDestroy { - event?: EventResponseWithLinks; +export class EventAccountingComponent implements OnInit, OnDestroy { + @Input() event?: EventResponseWithLinks; expenses: EventExpenseResponse[] = []; @@ -31,15 +31,7 @@ export class EventsViewAccountingComponent implements OnInit, OnDestroy { private toastService: ToastService, ) {} - ngOnInit(): void { - this.eventsService.event$.pipe(untilDestroyed(this)).subscribe((event) => { - this.event = event; - this.expenses = event?.expenses || []; - this.sortExpenes(); - - this.setActions(event); - }); - } + ngOnInit(): void {} ngOnDestroy() { this.modal?.dismiss(); diff --git a/frontend/src/app/modules/events/components/event-attendees/event-attendees.component.html b/frontend/src/app/modules/events/components/event-attendees/event-attendees.component.html new file mode 100644 index 000000000..609cb4c0b --- /dev/null +++ b/frontend/src/app/modules/events/components/event-attendees/event-attendees.component.html @@ -0,0 +1,31 @@ +

Účastníci

+ + + +

Žádní účastníci

+ + + +

{{ attendee.member | member: "nickname" }}

+

+
+ + {{ attendee.member?.groupId | group: "name" }} + + + + + + + +
+ +
+ Přidat účastníka +
+
diff --git a/frontend/src/app/modules/events/pages/events-view-attendees/events-view-attendees.component.scss b/frontend/src/app/modules/events/components/event-attendees/event-attendees.component.scss similarity index 100% rename from frontend/src/app/modules/events/pages/events-view-attendees/events-view-attendees.component.scss rename to frontend/src/app/modules/events/components/event-attendees/event-attendees.component.scss diff --git a/frontend/src/app/modules/events/components/event-attendees/event-attendees.component.ts b/frontend/src/app/modules/events/components/event-attendees/event-attendees.component.ts new file mode 100644 index 000000000..2fc49297f --- /dev/null +++ b/frontend/src/app/modules/events/components/event-attendees/event-attendees.component.ts @@ -0,0 +1,119 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { UntilDestroy } from "@ngneat/until-destroy"; +import { EventAttendeeResponse, EventResponseWithLinks } from "src/app/api"; +import { MemberSelectorModalComponent } from "src/app/modules/events/components/member-selector-modal/member-selector-modal.component"; +import { ApiService } from "src/app/services/api.service"; +import { ModalService } from "src/app/services/modal.service"; +import { ToastService } from "src/app/services/toast.service"; +import { Action } from "src/app/shared/components/action-buttons/action-buttons.component"; + +@UntilDestroy() +@Component({ + selector: "bo-event-attendees", + templateUrl: "./event-attendees.component.html", + styleUrls: ["./event-attendees.component.scss"], +}) +export class EventAttendeesComponent implements OnInit, OnDestroy { + @Input() event?: EventResponseWithLinks; + @Output() change = new EventEmitter(); + + attendees: EventAttendeeResponse[] = []; + + actions: Action[] = []; + + modal?: HTMLIonModalElement; + + constructor( + private api: ApiService, + private toastService: ToastService, + private modalService: ModalService, + ) {} + + ngOnInit(): void {} + + ngOnDestroy() { + this.modal?.dismiss(); + } + + private sortAttendees() { + this.attendees.sort((a, b) => { + const aString = a.member?.nickname || a.member?.firstName || a.member?.lastName || ""; + const bString = b.member?.nickname || b.member?.firstName || b.member?.lastName || ""; + return aString.localeCompare(bString); + }); + } + + async addAttendee() { + if (!this.event) return; + + const member = await this.modalService.componentModal(MemberSelectorModalComponent, {}); + + if (member) { + const attendees = this.event.attendees || []; + + if (attendees.some((item) => item.member && item.member.id === member.id)) { + this.toastService.toast("Účastník už v seznamu je."); + return; + } + + // optimistic update + attendees.push({ + memberId: member.id, + member, + eventId: this.event.id, + type: "attendee", + }); + + this.attendees = attendees; + this.sortAttendees(); + + try { + await this.api.events.addEventAttendee(this.event.id, member.id, {}); + this.toastService.toast("Účastník přidán."); + } catch (e) { + this.toastService.toast("Nepodařilo se přidat účastníka."); + } + + this.change.emit(); + } + } + + async removeAttendee(attendee: EventAttendeeResponse) { + if (!this.event) return; + + let attendees = this.event.attendees || []; + attendees = attendees.filter((item) => item.memberId !== attendee.memberId); + + this.attendees = attendees; // optimistic update + + await this.api.events.deleteEventAttendee(this.event.id, attendee.memberId); + + this.change.emit(); + } + + toggleSliding(sliding: any) { + sliding.getOpenAmount().then((open: number) => { + if (open) sliding.close(); + else sliding.open(); + }); + } + + private async exportExcel(event: EventResponseWithLinks) { + // TODO: + // if (event._links.["announcement-template"]) { + // const url = environment.apiRoot + event._links.["announcement-template"].href; + // window.open(url); + // } + } + + private setActions(event?: EventResponseWithLinks) { + this.actions = [ + { + text: "Stáhnout ohlášku", + icon: "download-outline", + // hidden: !event?._links.self.allowed.GET, // TODO: + handler: () => this.exportExcel(event!), + }, + ]; + } +} diff --git a/frontend/src/app/modules/events/components/event-expenses-chart/event-expenses-chart.component.ts b/frontend/src/app/modules/events/components/event-expenses-chart/event-expenses-chart.component.ts index d6fc53f09..8964148f9 100644 --- a/frontend/src/app/modules/events/components/event-expenses-chart/event-expenses-chart.component.ts +++ b/frontend/src/app/modules/events/components/event-expenses-chart/event-expenses-chart.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from "@angular/core"; +import { Component, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core"; import { DateTime } from "luxon"; import { EventResponseWithLinks } from "src/app/api"; import { EventExpenseTypes, EventExpenseTypesMetadata } from "src/app/config/event-expense-types"; @@ -8,7 +8,9 @@ import { EventExpenseTypes, EventExpenseTypesMetadata } from "src/app/config/eve templateUrl: "./event-expenses-chart.component.html", styleUrls: ["./event-expenses-chart.component.scss"], }) -export class EventExpensesChartComponent implements OnInit { +export class EventExpensesChartComponent implements OnInit, OnChanges { + @Input() event?: EventResponseWithLinks; + days: number = 0; persons: number = 0; @@ -16,7 +18,19 @@ export class EventExpensesChartComponent implements OnInit { totalByType: { [type: string]: { total: number; type?: EventExpenseTypesMetadata } } = {}; - @Input() set event(event: EventResponseWithLinks) { + ngOnChanges(changes: SimpleChanges): void { + if (changes["event"]) this.updateChart(this.event); + } + private updateChart(event?: EventResponseWithLinks) { + if (!event) { + this.days = 0; + this.persons = 0; + this.total = 0; + this.totalByType = {}; + + return; + } + const dateFrom = DateTime.fromISO(event.dateFrom).set({ hour: 0, minute: 0, second: 0, millisecond: 0 }); const dateTill = DateTime.fromISO(event.dateTill) .set({ hour: 0, minute: 0, second: 0, millisecond: 0 }) diff --git a/frontend/src/app/modules/events/components/event-info/event-info.component.html b/frontend/src/app/modules/events/components/event-info/event-info.component.html new file mode 100644 index 000000000..6ad486d68 --- /dev/null +++ b/frontend/src/app/modules/events/components/event-info/event-info.component.html @@ -0,0 +1,35 @@ +

Základní informace

+ + + +

{{ event?.name }}

+
+ + +

{{ [event?.dateFrom, event?.dateTill] | dateRange }}

+
+ + + + +
+ +

Popis

+ + + +

{{ event.description }}

+
+
+
diff --git a/frontend/src/app/modules/events/pages/events-view-info/events-view-info.component.scss b/frontend/src/app/modules/events/components/event-info/event-info.component.scss similarity index 62% rename from frontend/src/app/modules/events/pages/events-view-info/events-view-info.component.scss rename to frontend/src/app/modules/events/components/event-info/event-info.component.scss index 12746e7ee..c46238e31 100644 --- a/frontend/src/app/modules/events/pages/events-view-info/events-view-info.component.scss +++ b/frontend/src/app/modules/events/components/event-info/event-info.component.scss @@ -1,3 +1,7 @@ +:host { + display: block; +} + .attendees { ion-chip { color: #fff; diff --git a/frontend/src/app/modules/events/components/event-info/event-info.component.ts b/frontend/src/app/modules/events/components/event-info/event-info.component.ts new file mode 100644 index 000000000..7a1b55de7 --- /dev/null +++ b/frontend/src/app/modules/events/components/event-info/event-info.component.ts @@ -0,0 +1,75 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { UntilDestroy } from "@ngneat/until-destroy"; +import { EventResponse, EventResponseWithLinks } from "src/app/api"; +import { ModalService } from "src/app/services/modal.service"; + +@UntilDestroy() +@Component({ + selector: "bo-event-info", + templateUrl: "./event-info.component.html", + styleUrls: ["./event-info.component.scss"], +}) +export class EventInfoComponent implements OnInit, OnDestroy { + @Input() event?: EventResponseWithLinks; + @Output() update = new EventEmitter>(); + + constructor(private modalService: ModalService) {} + + ngOnInit() {} + + ngOnDestroy() {} + + async editName() { + const result = await this.modalService.inputModal({ + header: "Název akce", + inputs: { + name: { + placeholder: "Název", + type: "text", + value: this.event?.name, + }, + }, + }); + + if (result) this.update.emit(result); + } + + async editDate() { + const result = await this.modalService.inputModal({ + header: "Datum akce", + inputs: { + dateFrom: { + placeholder: "Datum od", + type: "date", + value: this.event?.dateFrom, + }, + dateTill: { + placeholder: "Datum do", + type: "date", + value: this.event?.dateTill, + }, + }, + }); + + if (result) this.update.emit(result); + } + + async editDescription() { + const result = await this.modalService.inputModal({ + header: "Popis akce", + inputs: { + description: { + placeholder: "Popis", + type: "text", + value: this.event?.description, + }, + }, + }); + + if (result) this.update.emit(result); + } + + updateType(type: string) { + this.update.emit({ type }); + } +} diff --git a/frontend/src/app/modules/events/components/event-registration/event-registration.component.html b/frontend/src/app/modules/events/components/event-registration/event-registration.component.html new file mode 100644 index 000000000..3308ff3b3 --- /dev/null +++ b/frontend/src/app/modules/events/components/event-registration/event-registration.component.html @@ -0,0 +1,34 @@ +

Přihláška

+ + + + + Přihláška nahrána. + Přihláška chybí. + Nahrát + Stáhnout + Smazat + + + + +

Nahrávám…

+ diff --git a/frontend/src/app/modules/events/pages/events-view-registration/events-view-registration.component.scss b/frontend/src/app/modules/events/components/event-registration/event-registration.component.scss similarity index 100% rename from frontend/src/app/modules/events/pages/events-view-registration/events-view-registration.component.scss rename to frontend/src/app/modules/events/components/event-registration/event-registration.component.scss diff --git a/frontend/src/app/modules/events/pages/events-view-registration/events-view-registration.component.ts b/frontend/src/app/modules/events/components/event-registration/event-registration.component.ts similarity index 83% rename from frontend/src/app/modules/events/pages/events-view-registration/events-view-registration.component.ts rename to frontend/src/app/modules/events/components/event-registration/event-registration.component.ts index 8977e7da7..0a64ea7c9 100644 --- a/frontend/src/app/modules/events/pages/events-view-registration/events-view-registration.component.ts +++ b/frontend/src/app/modules/events/components/event-registration/event-registration.component.ts @@ -1,7 +1,6 @@ -import { Component, ElementRef, ViewChild } from "@angular/core"; +import { Component, ElementRef, Input, ViewChild } from "@angular/core"; import { DomSanitizer } from "@angular/platform-browser"; import { UntilDestroy } from "@ngneat/until-destroy"; -import { filter } from "rxjs/operators"; import { EventResponseWithLinks } from "src/app/api"; import { ApiService } from "src/app/services/api.service"; import { ToastService } from "src/app/services/toast.service"; @@ -10,12 +9,12 @@ import { EventsService } from "../../services/events.service"; @UntilDestroy() @Component({ - selector: "events-view-registration", - templateUrl: "./events-view-registration.component.html", - styleUrls: ["./events-view-registration.component.scss"], + selector: "bo-event-registration", + templateUrl: "./event-registration.component.html", + styleUrls: ["./event-registration.component.scss"], }) -export class EventsViewRegistrationComponent { - event?: EventResponseWithLinks; +export class EventRegistrationComponent { + @Input() event?: EventResponseWithLinks; uploadingRegistration: boolean = false; @@ -28,14 +27,7 @@ export class EventsViewRegistrationComponent { private toastService: ToastService, private eventService: EventsService, private sanitizer: DomSanitizer, - ) { - this.eventService.event$.pipe(filter((event) => !!event)).subscribe((event) => this.updateEvent(event!)); - } - - private updateEvent(event: EventResponseWithLinks) { - this.event = event; - this.setActions(event); - } + ) {} uploadRegistrationSelect() { this.registrationInput.nativeElement.click(); diff --git a/frontend/src/app/modules/events/components/event-report/event-report.component.html b/frontend/src/app/modules/events/components/event-report/event-report.component.html new file mode 100644 index 000000000..eb7f843af --- /dev/null +++ b/frontend/src/app/modules/events/components/event-report/event-report.component.html @@ -0,0 +1 @@ +

Report

diff --git a/frontend/src/app/modules/events/pages/events-view-report/events-view-report.component.scss b/frontend/src/app/modules/events/components/event-report/event-report.component.scss similarity index 100% rename from frontend/src/app/modules/events/pages/events-view-report/events-view-report.component.scss rename to frontend/src/app/modules/events/components/event-report/event-report.component.scss diff --git a/frontend/src/app/modules/events/components/event-report/event-report.component.ts b/frontend/src/app/modules/events/components/event-report/event-report.component.ts new file mode 100644 index 000000000..871d179ee --- /dev/null +++ b/frontend/src/app/modules/events/components/event-report/event-report.component.ts @@ -0,0 +1,13 @@ +import { Component, Input } from "@angular/core"; +import { UntilDestroy } from "@ngneat/until-destroy"; +import { EventResponseWithLinks } from "src/app/api"; + +@UntilDestroy() +@Component({ + selector: "bo-event-report", + templateUrl: "./event-report.component.html", + styleUrls: ["./event-report.component.scss"], +}) +export class EventReportComponent { + @Input() event?: EventResponseWithLinks; +} diff --git a/frontend/src/app/modules/events/components/event-subtype-selector/event-subtype-selector.component.html b/frontend/src/app/modules/events/components/event-type-selector/event-type-selector.component.html similarity index 100% rename from frontend/src/app/modules/events/components/event-subtype-selector/event-subtype-selector.component.html rename to frontend/src/app/modules/events/components/event-type-selector/event-type-selector.component.html diff --git a/frontend/src/app/modules/events/components/event-subtype-selector/event-subtype-selector.component.scss b/frontend/src/app/modules/events/components/event-type-selector/event-type-selector.component.scss similarity index 94% rename from frontend/src/app/modules/events/components/event-subtype-selector/event-subtype-selector.component.scss rename to frontend/src/app/modules/events/components/event-type-selector/event-type-selector.component.scss index b90c376b4..0f1e5914e 100644 --- a/frontend/src/app/modules/events/components/event-subtype-selector/event-subtype-selector.component.scss +++ b/frontend/src/app/modules/events/components/event-type-selector/event-type-selector.component.scss @@ -1,7 +1,7 @@ $size: 30px; :host { - display: block; + display: inline-block; } span.type { diff --git a/frontend/src/app/modules/events/components/event-subtype-selector/event-subtype-selector.component.ts b/frontend/src/app/modules/events/components/event-type-selector/event-type-selector.component.ts similarity index 80% rename from frontend/src/app/modules/events/components/event-subtype-selector/event-subtype-selector.component.ts rename to frontend/src/app/modules/events/components/event-type-selector/event-type-selector.component.ts index 0f0e59a35..c4149653e 100644 --- a/frontend/src/app/modules/events/components/event-subtype-selector/event-subtype-selector.component.ts +++ b/frontend/src/app/modules/events/components/event-type-selector/event-type-selector.component.ts @@ -3,14 +3,14 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; import { EventTypeID, EventTypes } from "src/app/config/event-types"; @Component({ - selector: "event-subtype-selector", - templateUrl: "./event-subtype-selector.component.html", - styleUrls: ["./event-subtype-selector.component.scss"], + selector: "bo-event-type-selector", + templateUrl: "./event-type-selector.component.html", + styleUrls: ["./event-type-selector.component.scss"], providers: [ { provide: NG_VALUE_ACCESSOR, multi: true, - useExisting: forwardRef(() => EventSubtypeSelectorComponent), + useExisting: forwardRef(() => EventTypeSelectorComponent), }, ], host: { @@ -18,14 +18,14 @@ import { EventTypeID, EventTypes } from "src/app/config/event-types"; "[class.readonly]": "readonly", }, }) -export class EventSubtypeSelectorComponent implements ControlValueAccessor, AfterViewInit { +export class EventTypeSelectorComponent implements ControlValueAccessor, AfterViewInit { value?: EventTypeID; types = EventTypes; onChange: any = () => {}; onTouched: any = () => {}; - disabled: boolean = false; + @Input() disabled: boolean = false; @Input() readonly: boolean = false; constructor(private elRef: ElementRef) {} @@ -42,7 +42,7 @@ export class EventSubtypeSelectorComponent implements ControlValueAccessor, Afte } /* ControlValueAccessor */ - writeValue(value: EventTypeID) { + writeValue(value?: EventTypeID) { this.value = value; } diff --git a/frontend/src/app/modules/events/components/member-selector-modal/member-selector-modal.component.html b/frontend/src/app/modules/events/components/member-selector-modal/member-selector-modal.component.html index 1f855370f..57498d20c 100644 --- a/frontend/src/app/modules/events/components/member-selector-modal/member-selector-modal.component.html +++ b/frontend/src/app/modules/events/components/member-selector-modal/member-selector-modal.component.html @@ -5,18 +5,18 @@ (ionChange)="searchMembers($any($event).detail.value)" > - Zrušit + Zrušit - + -

{{ member | member : "nickname" }}

+

{{ member | member: "nickname" }}

- + {{ member.group }}
diff --git a/frontend/src/app/modules/events/components/member-selector-modal/member-selector-modal.component.ts b/frontend/src/app/modules/events/components/member-selector-modal/member-selector-modal.component.ts index 8295d207e..fd193140d 100644 --- a/frontend/src/app/modules/events/components/member-selector-modal/member-selector-modal.component.ts +++ b/frontend/src/app/modules/events/components/member-selector-modal/member-selector-modal.component.ts @@ -2,13 +2,14 @@ import { Component, Input, OnInit, ViewChild } from "@angular/core"; import { IonSearchbar, ModalController, ViewDidEnter } from "@ionic/angular"; import { MemberResponse } from "src/app/api"; import { ApiService } from "src/app/services/api.service"; +import { ModalComponent } from "src/app/services/modal.service"; @Component({ selector: "bo-member-selector-modal", templateUrl: "./member-selector-modal.component.html", styleUrls: ["./member-selector-modal.component.scss"], }) -export class MemberSelectorModalComponent implements OnInit, ViewDidEnter { +export class MemberSelectorModalComponent extends ModalComponent implements OnInit, ViewDidEnter { @Input() members: MemberResponse[] = []; membersIndex: string[] = []; @@ -17,7 +18,12 @@ export class MemberSelectorModalComponent implements OnInit, ViewDidEnter { @ViewChild("searchBar") searchBar!: IonSearchbar; - constructor(private modalController: ModalController, private api: ApiService) {} + constructor( + private api: ApiService, + modalController: ModalController, + ) { + super(modalController); + } ngOnInit(): void { this.loadMembers(); @@ -33,11 +39,13 @@ export class MemberSelectorModalComponent implements OnInit, ViewDidEnter { } ionViewDidEnter() { - // TODO: remove setTimeout when following bug gets resolved - // https://github.com/ionic-team/ionic-framework/issues/17745 window.setTimeout(() => this.searchBar.setFocus(), 300); } + selectMember(member: MemberResponse) { + this.submit.emit(member); + } + searchMembers(searchString?: string) { if (!searchString) { this.filteredMembers = this.members; @@ -63,8 +71,4 @@ export class MemberSelectorModalComponent implements OnInit, ViewDidEnter { return aString.localeCompare(bString); }); } - - close(member?: MemberResponse) { - this.modalController.dismiss({ member: member }); - } } diff --git a/frontend/src/app/modules/events/events-routing.module.ts b/frontend/src/app/modules/events/events-routing.module.ts index 837b19813..947d51efe 100644 --- a/frontend/src/app/modules/events/events-routing.module.ts +++ b/frontend/src/app/modules/events/events-routing.module.ts @@ -1,33 +1,13 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; - -import { EventsListComponent } from "./pages/events-list/events-list.component"; - -import { EventsComponent } from "./events.component"; -import { EventEditComponent } from "./pages/event-edit/event-edit.component"; +import { EventViewComponent } from "./pages/event-view/event-view.component"; import { EventsCreateComponent } from "./pages/events-create/events-create.component"; -import { EventsViewAccountingComponent } from "./pages/events-view-accounting/events-view-accounting.component"; -import { EventsViewAttendeesComponent } from "./pages/events-view-attendees/events-view-attendees.component"; -import { EventsViewInfoComponent } from "./pages/events-view-info/events-view-info.component"; -import { EventsViewRegistrationComponent } from "./pages/events-view-registration/events-view-registration.component"; -import { EventsViewReportComponent } from "./pages/events-view-report/events-view-report.component"; +import { EventsListComponent } from "./pages/events-list/events-list.component"; const routes: Routes = [ { path: "", component: EventsListComponent }, { path: "vytvorit", component: EventsCreateComponent }, - { - path: ":event", - component: EventsComponent, - children: [ - { path: "upravit", component: EventEditComponent }, - { path: "info", component: EventsViewInfoComponent }, - { path: "ucastnici", component: EventsViewAttendeesComponent }, - { path: "prihlaska", component: EventsViewRegistrationComponent }, - { path: "uctovani", component: EventsViewAccountingComponent }, - { path: "report", component: EventsViewReportComponent }, - { path: "", redirectTo: "info", pathMatch: "full" }, - ], - }, + { path: ":event", component: EventViewComponent }, ]; @NgModule({ diff --git a/frontend/src/app/modules/events/events.component.html b/frontend/src/app/modules/events/events.component.html deleted file mode 100644 index 16ee7b722..000000000 --- a/frontend/src/app/modules/events/events.component.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - Akce -    - - - - Přihláška -    -    - - - - Účastníci - - {{ (event?.leaders?.length || 0) + (event?.attendees?.length || 0) }} - - - - - Účtování - {{ event?.expenses?.length }} - - - - Report -    -    - - - diff --git a/frontend/src/app/modules/events/events.component.scss b/frontend/src/app/modules/events/events.component.scss deleted file mode 100644 index 75e804f26..000000000 --- a/frontend/src/app/modules/events/events.component.scss +++ /dev/null @@ -1,29 +0,0 @@ -@import "src/styles/variables"; - -.right { - border-left: 1px solid #ccc; -} - -.block { - padding: 30px 0; -} -.actions { - width: 100%; -} - -p.description { - max-width: 700px; -} - -.dropdown-item.disabled { - opacity: 0.3; -} // https://github.com/twbs/bootstrap/issues/28666 - -@media (max-width: 767.98px) { - .actions .dropdown-toggle { - width: 100%; - } - .actions .dropdown-menu { - width: 100%; - } -} diff --git a/frontend/src/app/modules/events/events.component.ts b/frontend/src/app/modules/events/events.component.ts deleted file mode 100644 index 6a91b9973..000000000 --- a/frontend/src/app/modules/events/events.component.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute, Params } from "@angular/router"; -import { NavController } from "@ionic/angular"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { filter } from "rxjs/operators"; -import { EventResponseWithLinks } from "src/app/api"; -import { ToastService } from "src/app/services/toast.service"; -import { Action } from "src/app/shared/components/action-buttons/action-buttons.component"; -import { EventsService } from "./services/events.service"; - -@UntilDestroy() -@Component({ - selector: "bo-events", - templateUrl: "./events.component.html", - styleUrls: ["./events.component.scss"], -}) -export class EventsComponent implements OnInit { - event?: EventResponseWithLinks; - - actions: Action[] = []; - - constructor( - private route: ActivatedRoute, - private eventsService: EventsService, - private navController: NavController, - private toastService: ToastService, - ) {} - - ngOnInit() { - this.route.params.pipe(untilDestroyed(this)).subscribe((params: Params) => this.loadEvent(params.event)); - - this.eventsService.event$.pipe(filter((event) => !!event)).subscribe((event) => (this.event = event)); - } - - async loadEvent(eventId: number) { - try { - await this.eventsService.loadEvent(eventId); - } catch (err) { - this.navController.navigateBack("/akce"); - this.toastService.toast("Akce nebyla nalezena, zobrazuji přehled akcí."); - } - } -} diff --git a/frontend/src/app/modules/events/events.module.ts b/frontend/src/app/modules/events/events.module.ts index 8ae8d86dc..9b97a978d 100644 --- a/frontend/src/app/modules/events/events.module.ts +++ b/frontend/src/app/modules/events/events.module.ts @@ -1,45 +1,45 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { SharedModule } from "src/app/shared/shared.module"; +import { EventAccountingComponent } from "./components/event-accounting/event-accounting.component"; import { EventAddAttendeesComponent } from "./components/event-add-attendees/event-add-attendees.component"; import { EventAgeHistogramComponent } from "./components/event-age-histogram/event-age-histogram.component"; +import { EventAttendeesComponent } from "./components/event-attendees/event-attendees.component"; import { EventBirthdayListComponent } from "./components/event-birthday-list/event-birthday-list.component"; import { EventExpenseModalComponent } from "./components/event-expense-modal/event-expense-modal.component"; import { EventExpensesChartComponent } from "./components/event-expenses-chart/event-expenses-chart.component"; -import { EventSubtypeSelectorComponent } from "./components/event-subtype-selector/event-subtype-selector.component"; +import { EventInfoComponent } from "./components/event-info/event-info.component"; +import { EventRegistrationComponent } from "./components/event-registration/event-registration.component"; +import { EventReportComponent } from "./components/event-report/event-report.component"; +import { EventTypeSelectorComponent } from "./components/event-type-selector/event-type-selector.component"; import { MemberSelectorModalComponent } from "./components/member-selector-modal/member-selector-modal.component"; import { MemberSelectorComponent } from "./components/member-selector/member-selector.component"; import { EventsRoutingModule } from "./events-routing.module"; -import { EventsComponent } from "./events.component"; import { EventEditComponent } from "./pages/event-edit/event-edit.component"; +import { EventViewComponent } from "./pages/event-view/event-view.component"; import { EventsCreateComponent } from "./pages/events-create/events-create.component"; import { EventsListComponent } from "./pages/events-list/events-list.component"; -import { EventsViewAccountingComponent } from "./pages/events-view-accounting/events-view-accounting.component"; -import { EventsViewAttendeesComponent } from "./pages/events-view-attendees/events-view-attendees.component"; -import { EventsViewInfoComponent } from "./pages/events-view-info/events-view-info.component"; -import { EventsViewRegistrationComponent } from "./pages/events-view-registration/events-view-registration.component"; -import { EventsViewReportComponent } from "./pages/events-view-report/events-view-report.component"; import { EventsService } from "./services/events.service"; @NgModule({ declarations: [ - EventsListComponent, - EventEditComponent, - EventsViewRegistrationComponent, + EventAccountingComponent, + EventAddAttendeesComponent, EventAgeHistogramComponent, + EventAttendeesComponent, EventBirthdayListComponent, - EventSubtypeSelectorComponent, + EventEditComponent, + EventExpenseModalComponent, EventExpensesChartComponent, + EventInfoComponent, + EventRegistrationComponent, + EventReportComponent, EventsCreateComponent, - EventsViewAttendeesComponent, - EventsViewInfoComponent, - EventsViewAccountingComponent, - EventAddAttendeesComponent, + EventsListComponent, + EventTypeSelectorComponent, + EventViewComponent, MemberSelectorComponent, MemberSelectorModalComponent, - EventExpenseModalComponent, - EventsViewReportComponent, - EventsComponent, ], imports: [CommonModule, EventsRoutingModule, SharedModule], providers: [EventsService], diff --git a/frontend/src/app/modules/events/pages/event-view/event-view.component.html b/frontend/src/app/modules/events/pages/event-view/event-view.component.html new file mode 100644 index 000000000..50a66ab3a --- /dev/null +++ b/frontend/src/app/modules/events/pages/event-view/event-view.component.html @@ -0,0 +1,118 @@ + + + +
+
+ +
+
+
+
+ + + +
+
+ +
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + diff --git a/frontend/src/app/modules/events/pages/event-view/event-view.component.scss b/frontend/src/app/modules/events/pages/event-view/event-view.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/modules/events/pages/event-view/event-view.component.ts b/frontend/src/app/modules/events/pages/event-view/event-view.component.ts new file mode 100644 index 000000000..f6c1ad20f --- /dev/null +++ b/frontend/src/app/modules/events/pages/event-view/event-view.component.ts @@ -0,0 +1,167 @@ +import { Component } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { ViewWillEnter, ViewWillLeave } from "@ionic/angular"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { EventResponseWithLinks } from "src/app/api"; +import { ApiService } from "src/app/services/api.service"; +import { ModalService } from "src/app/services/modal.service"; +import { ToastService } from "src/app/services/toast.service"; +import { Action } from "src/app/shared/components/action-buttons/action-buttons.component"; +import { ExtractExisting } from "src/helpers/typings"; + +export type EventStatusActions = ExtractExisting< + keyof EventResponseWithLinks["_links"], + "publishEvent" | "unpublishEvent" | "uncancelEvent" | "cancelEvent" | "rejectEvent" | "submitEvent" +>; + +@UntilDestroy() +@Component({ + selector: "bo-event-view", + templateUrl: "./event-view.component.html", + styleUrl: "./event-view.component.scss", +}) +export class EventViewComponent implements ViewWillEnter, ViewWillLeave { + event: any; + + actions: Action[] = []; + + view?: "info" | "attendees" | "accounting" | "registration" | "report"; + + constructor( + private readonly api: ApiService, + private readonly route: ActivatedRoute, + private readonly router: Router, + private readonly toastService: ToastService, + private readonly modalService: ModalService, + ) {} + + ionViewWillEnter(): void { + this.route.params + .pipe(untilDestroyed(this, "ionViewWillLeave")) + .subscribe((params) => this.loadEvent(parseInt(params.event))); + } + + ionViewWillLeave(): void {} + + async updateEvent(data: Partial) { + if (!this.event) return; + + try { + Object.assign(this.event, data); + await this.api.events.updateEvent(this.event.id, data); + this.toastService.toast("Uloženo."); + } catch (e) { + this.toastService.toast("Nepodařilo se uložit změny.", { color: "warning" }); + } + + await this.loadEvent(this.event.id); + } + + private async loadEvent(eventId: number) { + this.event = await this.api.events.getEvent(eventId).then((res) => res.data); + + this.setActions(this.event); + } + + async leadEvent(event: EventResponseWithLinks) { + await this.api.events.leadEvent(event.id); + + await this.loadEvent(event.id); + + this.toastService.toast("Uloženo"); + } + + private async eventStatusAction(event: EventResponseWithLinks, action: EventStatusActions) { + if (!event._links[action].allowed) { + this.toastService.toast("K této akci nemáš oprávnění."); + return; + } + + const statusNote = window.prompt("Poznámka ke změně stavu (můžeš nechat prázdné):"); + if (statusNote === null) return; // user clicked on cancel + + await this.api.events[action](event.id, { statusNote }); + + await this.loadEvent(event.id); + + this.toastService.toast("Uloženo"); + } + + private async deleteEvent(event: EventResponseWithLinks) { + const confirmation = await this.modalService.deleteConfirmationModal(`Opravdu chcete smazat akci ${event.name}?`); + + if (confirmation) { + await this.api.events.deleteEvent(event.id); + this.router.navigate(["/akce"], { relativeTo: this.route, replaceUrl: true }); + this.toastService.toast("Akce smazána"); + } + } + + private setActions(event: EventResponseWithLinks) { + this.actions = [ + { + text: "Vést akci", + color: "success", + icon: "hand-left-outline", + hidden: !event._links.leadEvent.allowed, + handler: () => this.leadEvent(event), + }, + { + text: "Upravit", + pinned: true, + icon: "create-outline", + hidden: !event._links.updateEvent.allowed, + handler: () => this.router.navigate(["../upravit"], { relativeTo: this.route }), + }, + { + text: "Ke schválení", + icon: "arrow-forward-outline", + color: "primary", + hidden: !event?._links.submitEvent.allowed, + handler: () => this.eventStatusAction(event, "submitEvent"), + }, + { + text: "Do programu", + icon: "arrow-forward-outline", + color: "primary", + hidden: !event?._links.publishEvent.allowed, + handler: () => this.eventStatusAction(event, "publishEvent"), + }, + { + text: "Vrátit k úpravám", + icon: "arrow-back-outline", + color: "danger", + hidden: !event?._links.rejectEvent.allowed, + handler: () => this.eventStatusAction(event, "rejectEvent"), + }, + { + text: "Odebrat z programu", + icon: "arrow-back-outline", + color: "danger", + hidden: !event?._links.unpublishEvent.allowed, + handler: () => this.eventStatusAction(event, "unpublishEvent"), + }, + { + text: "Označit jako zrušenou", + color: "danger", + icon: "arrow-back-outline", + hidden: !event?._links.cancelEvent.allowed, + handler: () => this.eventStatusAction(event, "cancelEvent"), + }, + { + text: "Odzrušit", + icon: "arrow-forward-outline", + hidden: !event?._links.uncancelEvent.allowed, + handler: () => this.eventStatusAction(event, "uncancelEvent"), + }, + { + text: "Smazat", + role: "destructive", + color: "danger", + icon: "trash-outline", + hidden: !event._links.deleteEvent.allowed, + handler: () => this.deleteEvent(event), + }, + ]; + } +} diff --git a/frontend/src/app/modules/events/pages/events-view-accounting/events-view-accounting.component.html b/frontend/src/app/modules/events/pages/events-view-accounting/events-view-accounting.component.html deleted file mode 100644 index 8d7f158ca..000000000 --- a/frontend/src/app/modules/events/pages/events-view-accounting/events-view-accounting.component.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - Účtování: {{ event?.name }} - - - - - - - - - - - -

- {{ expense.id }} - {{ expense.description }} - -

-

{{ expense.amount | number: "1.2-2" : "cs" }} Kč

-
- - {{ expense.type }} - - - {{ expense.amount | number: "1.2-2" : "cs" }} Kč - -
- - - - - - - - -
-
-
diff --git a/frontend/src/app/modules/events/pages/events-view-attendees/events-view-attendees.component.html b/frontend/src/app/modules/events/pages/events-view-attendees/events-view-attendees.component.html deleted file mode 100644 index ab6cbd372..000000000 --- a/frontend/src/app/modules/events/pages/events-view-attendees/events-view-attendees.component.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - Účastníci: {{ event?.name }} - - - - - - - - - - - -

{{ attendee.member | member : "nickname" }}

-

-
- - {{ attendee.member?.groupId | group : "name" }} - - - - - - - -
-
-
diff --git a/frontend/src/app/modules/events/pages/events-view-attendees/events-view-attendees.component.ts b/frontend/src/app/modules/events/pages/events-view-attendees/events-view-attendees.component.ts deleted file mode 100644 index c61830756..000000000 --- a/frontend/src/app/modules/events/pages/events-view-attendees/events-view-attendees.component.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ModalController } from "@ionic/angular"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { EventAttendeeResponse, EventResponseWithLinks, MemberResponse } from "src/app/api"; -import { MemberSelectorModalComponent } from "src/app/modules/events/components/member-selector-modal/member-selector-modal.component"; -import { EventsService } from "src/app/modules/events/services/events.service"; -import { ApiService } from "src/app/services/api.service"; -import { ToastService } from "src/app/services/toast.service"; -import { Action } from "src/app/shared/components/action-buttons/action-buttons.component"; - -@UntilDestroy() -@Component({ - selector: "bo-events-view-attendees", - templateUrl: "./events-view-attendees.component.html", - styleUrls: ["./events-view-attendees.component.scss"], -}) -export class EventsViewAttendeesComponent implements OnInit, OnDestroy { - event?: EventResponseWithLinks; - - attendees: EventAttendeeResponse[] = []; - - actions: Action[] = []; - - modal?: HTMLIonModalElement; - - constructor( - private eventsService: EventsService, - private api: ApiService, - public modalController: ModalController, - private toastService: ToastService, - ) {} - - ngOnInit(): void { - this.eventsService.event$.pipe(untilDestroyed(this)).subscribe((event) => { - this.event = event || undefined; - this.attendees = event?.attendees || []; - - this.sortAttendees(); - - this.setActions(this.event); - }); - } - - ngOnDestroy() { - this.modal?.dismiss(); - } - - private sortAttendees() { - this.attendees.sort((a, b) => { - const aString = a.member?.nickname || a.member?.firstName || a.member?.lastName || ""; - const bString = b.member?.nickname || b.member?.firstName || b.member?.lastName || ""; - return aString.localeCompare(bString); - }); - } - - async addAttendeeModal() { - this.modal = await this.modalController.create({ - component: MemberSelectorModalComponent, - }); - - this.modal.onWillDismiss().then((ev) => { - if (ev.data?.member) this.addAttendee(ev.data?.member); - }); - - return await this.modal.present(); - } - - private async addAttendee(member: MemberResponse) { - if (!this.event) return; - - const attendees = this.event.attendees || []; - - if (attendees.some((item) => item.member && item.member.id === member.id)) { - this.toastService.toast("Účastník už v seznamu je."); - return; - } - - // optimistic update - attendees.push({ - memberId: member.id, - member, - eventId: this.event.id, - type: "attendee", - }); - - this.attendees = attendees; - this.sortAttendees(); - - await this.api.events.addEventAttendee(this.event.id, member.id, {}); - - await this.eventsService.loadEvent(this.event.id); - } - - async removeAttendee(attendee: EventAttendeeResponse) { - if (!this.event) return; - - let attendees = this.event.attendees || []; - attendees = attendees.filter((item) => item.memberId !== attendee.memberId); - - this.attendees = attendees; // optimistic update - - await this.api.events.deleteEventAttendee(this.event.id, attendee.memberId); - - await this.eventsService.loadEvent(this.event.id); - } - - toggleSliding(sliding: any) { - sliding.getOpenAmount().then((open: number) => { - if (open) sliding.close(); - else sliding.open(); - }); - } - - private async exportExcel(event: EventResponseWithLinks) { - // TODO: - // if (event._links.["announcement-template"]) { - // const url = environment.apiRoot + event._links.["announcement-template"].href; - // window.open(url); - // } - } - - private setActions(event?: EventResponseWithLinks) { - this.actions = [ - { - text: "Přidat", - icon: "add-outline", - pinned: true, - hidden: !event?._links.addEventAttendee.allowed, - handler: () => this.addAttendeeModal(), - }, - { - text: "Stáhnout ohlášku", - icon: "download-outline", - // hidden: !event?._links.self.allowed.GET, // TODO: - handler: () => this.exportExcel(event!), - }, - ]; - } -} diff --git a/frontend/src/app/modules/events/pages/events-view-info/events-view-info.component.html b/frontend/src/app/modules/events/pages/events-view-info/events-view-info.component.html deleted file mode 100644 index 6406ea3c6..000000000 --- a/frontend/src/app/modules/events/pages/events-view-info/events-view-info.component.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - -

-

-
- - -
-
-

Účastníci

-
- - {{ attendee.member | member : "nickname" }} - -

Účastníci zatím chybí

-
- - - -

Účtování

- -

Účtování zatím chybí

- -

Fotogalerie

- -

Fotky zatím chybí

-
- -
- -
-
-
-
diff --git a/frontend/src/app/modules/events/pages/events-view-info/events-view-info.component.ts b/frontend/src/app/modules/events/pages/events-view-info/events-view-info.component.ts deleted file mode 100644 index a16a24a89..000000000 --- a/frontend/src/app/modules/events/pages/events-view-info/events-view-info.component.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { AlertController } from "@ionic/angular"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { filter } from "rxjs/operators"; -import { AlbumResponseWithLinks, EventResponseWithLinks } from "src/app/api"; -import { EventStatuses } from "src/app/config/event-statuses"; -import { ApiService } from "src/app/services/api.service"; -import { ToastService } from "src/app/services/toast.service"; -import { Action } from "src/app/shared/components/action-buttons/action-buttons.component"; -import { EventActions } from "../../schema/event-actions"; -import { EventsService } from "../../services/events.service"; - -@UntilDestroy() -@Component({ - selector: "bo-events-view-info", - templateUrl: "./events-view-info.component.html", - styleUrls: ["./events-view-info.component.scss"], -}) -export class EventsViewInfoComponent implements OnInit, OnDestroy { - event?: EventResponseWithLinks; - - eventAlbum?: AlbumResponseWithLinks; - - actions: Action[] = []; - - view: "event" | "attendees" | "accounting" | "registration" | "report" = "event"; - - statuses = EventStatuses; - - constructor( - private api: ApiService, - private router: Router, - private route: ActivatedRoute, - private toastService: ToastService, - private eventsService: EventsService, - private alertConctroller: AlertController, - ) {} - - ngOnInit() { - this.eventsService.event$ - .pipe(untilDestroyed(this)) - .pipe(filter((event) => !!event)) - .subscribe((event) => this.updateEvent(event!)); - } - - ngOnDestroy() {} - - async updateEvent(event: EventResponseWithLinks) { - this.event = event; - - this.actions = this.getActions(this.event); - } - - async deleteEvent(event: EventResponseWithLinks) { - const alert = await this.alertConctroller.create({ - header: "Smazat akci?", - message: `Opravdu chcete smazat akci ${event.name}?`, - buttons: [ - { text: "Zrušit", role: "cancel" }, - { text: "Smazat", handler: () => this.deleteEventConfirmed(event) }, - ], - }); - - await alert.present(); - } - - async deleteEventConfirmed(event: EventResponseWithLinks) { - await this.eventsService.deleteEvent(event.id); - - this.router.navigate(["/akce"], { relativeTo: this.route, replaceUrl: true }); - this.toastService.toast("Akce smazána"); - } - - async leadEvent(event: EventResponseWithLinks) { - await this.api.events.leadEvent(event.id); - - await this.eventsService.loadEvent(event.id); - - this.toastService.toast("Uloženo"); - } - - async eventAction(event: EventResponseWithLinks, action: EventActions) { - if (!event._links[action].allowed) { - this.toastService.toast("K této akci nemáš oprávnění."); - return; - } - - const statusNote = window.prompt("Poznámka ke změně stavu (můžeš nechat prázdné):"); - if (statusNote === null) return; // user clicked on cancel - - await this.api.events[action](event.id, { statusNote }); - - await this.eventsService.loadEvent(event.id); - - this.toastService.toast("Uloženo"); - } - - private getActions(event: EventResponseWithLinks): Action[] { - return [ - { - text: "Vést akci", - color: "success", - icon: "hand-left-outline", - hidden: !event._links.leadEvent.allowed, - handler: () => this.leadEvent(event), - }, - { - text: "Upravit", - pinned: true, - icon: "create-outline", - hidden: !event._links.updateEvent.allowed, - handler: () => this.router.navigate(["../upravit"], { relativeTo: this.route }), - }, - { - text: "Ke schválení", - icon: "arrow-forward-outline", - color: "primary", - hidden: !event?._links.submitEvent.allowed, - handler: () => this.eventAction(event, "submitEvent"), - }, - { - text: "Do programu", - icon: "arrow-forward-outline", - color: "primary", - hidden: !event?._links.publishEvent.allowed, - handler: () => this.eventAction(event, "publishEvent"), - }, - { - text: "Vrátit k úpravám", - icon: "arrow-back-outline", - color: "danger", - hidden: !event?._links.rejectEvent.allowed, - handler: () => this.eventAction(event, "rejectEvent"), - }, - { - text: "Odebrat z programu", - icon: "arrow-back-outline", - color: "danger", - hidden: !event?._links.unpublishEvent.allowed, - handler: () => this.eventAction(event, "unpublishEvent"), - }, - { - text: "Označit jako zrušenou", - color: "danger", - icon: "arrow-back-outline", - hidden: !event?._links.cancelEvent.allowed, - handler: () => this.eventAction(event, "cancelEvent"), - }, - { - text: "Odzrušit", - icon: "arrow-forward-outline", - hidden: !event?._links.uncancelEvent.allowed, - handler: () => this.eventAction(event, "uncancelEvent"), - }, - { - text: "Smazat", - role: "destructive", - color: "danger", - icon: "trash-outline", - hidden: !event._links.deleteEvent.allowed, - handler: () => this.deleteEvent(event), - }, - ]; - } -} diff --git a/frontend/src/app/modules/events/pages/events-view-registration/events-view-registration.component.html b/frontend/src/app/modules/events/pages/events-view-registration/events-view-registration.component.html deleted file mode 100644 index 53f7f105d..000000000 --- a/frontend/src/app/modules/events/pages/events-view-registration/events-view-registration.component.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - Přihláška: {{ event?.name }} - - - - - - - - - Stáhnout přihlášku - - -

Nahrávám…

- -
diff --git a/frontend/src/app/modules/events/pages/events-view-report/events-view-report.component.html b/frontend/src/app/modules/events/pages/events-view-report/events-view-report.component.html deleted file mode 100644 index 5f6199a0b..000000000 --- a/frontend/src/app/modules/events/pages/events-view-report/events-view-report.component.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Report: {{ event?.name }} - - - - - - - diff --git a/frontend/src/app/modules/events/pages/events-view-report/events-view-report.component.ts b/frontend/src/app/modules/events/pages/events-view-report/events-view-report.component.ts deleted file mode 100644 index 588df4bce..000000000 --- a/frontend/src/app/modules/events/pages/events-view-report/events-view-report.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Component, OnInit } from "@angular/core"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { EventResponseWithLinks } from "src/app/api"; -import { EventsService } from "src/app/modules/events/services/events.service"; -import { Action } from "src/app/shared/components/action-buttons/action-buttons.component"; - -@UntilDestroy() -@Component({ - selector: "bo-events-view-report", - templateUrl: "./events-view-report.component.html", - styleUrls: ["./events-view-report.component.scss"], -}) -export class EventsViewReportComponent implements OnInit { - event?: EventResponseWithLinks; - - actions: Action[] = []; - - constructor(private eventsService: EventsService) {} - - ngOnInit(): void { - this.eventsService.event$.pipe(untilDestroyed(this)).subscribe((event) => (this.event = event)); - } -} diff --git a/frontend/src/app/modules/events/schema/event-actions.ts b/frontend/src/app/modules/events/schema/event-actions.ts deleted file mode 100644 index d191a5bfe..000000000 --- a/frontend/src/app/modules/events/schema/event-actions.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { EventsApi } from "src/app/api"; -import { ExtractExisting } from "src/helpers/typings"; - -export type EventActions = ExtractExisting< - keyof EventsApi, - "publishEvent" | "unpublishEvent" | "uncancelEvent" | "cancelEvent" | "rejectEvent" | "submitEvent" ->; - -// TODO: leadEvent diff --git a/frontend/src/app/modules/members/pages/members-view/members-view.component.html b/frontend/src/app/modules/members/pages/members-view/members-view.component.html index d7a86e9ed..837bc693b 100644 --- a/frontend/src/app/modules/members/pages/members-view/members-view.component.html +++ b/frontend/src/app/modules/members/pages/members-view/members-view.component.html @@ -5,9 +5,8 @@
diff --git a/frontend/src/app/services/modal.service.ts b/frontend/src/app/services/modal.service.ts index c6764b746..80573ef2a 100644 --- a/frontend/src/app/services/modal.service.ts +++ b/frontend/src/app/services/modal.service.ts @@ -1,6 +1,6 @@ -import { Injectable } from "@angular/core"; -import { AlertController } from "@ionic/angular"; -import { TextFieldTypes } from "@ionic/core"; +import { EventEmitter, Injectable } from "@angular/core"; +import { AlertController, ModalController } from "@ionic/angular"; +import { ComponentProps, TextFieldTypes } from "@ionic/core"; interface BaseModalOptions { header?: string; @@ -13,7 +13,7 @@ interface InputModalOptions> extends BaseModalOpti inputs: { [K in keyof D]: InputModalInput }; } -interface InputModalInput { +export interface InputModalInput { type?: T extends number ? "number" : T extends boolean ? "checkbox" : Exclude | "textarea"; placeholder?: string; value?: T; @@ -24,11 +24,29 @@ interface SelectModalOptions extends BaseModalOptions { value?: D; } +export class ModalComponent { + submit = new EventEmitter(); + close = new EventEmitter(); + + constructor(private modalCtrl: ModalController) { + this.submit.subscribe((data) => this.modalCtrl.dismiss(data)); + this.close.subscribe(() => this.modalCtrl.dismiss(null)); + } +} + +type ModalComponentRef = { new (...args: any): ModalComponent }; + +type ModalComponentData = + InstanceType extends { submit: EventEmitter } ? D : never; + @Injectable({ providedIn: "root", }) export class ModalService { - constructor(private alertController: AlertController) {} + constructor( + private alertController: AlertController, + private modalController: ModalController, + ) {} async deleteConfirmationModal(message: string, options: DeleteConfirmationModalOptions = {}) { return new Promise(async (resolve, reject) => { @@ -94,4 +112,17 @@ export class ModalService { await alert.present(); }); } + + async componentModal(component: C, componentProps?: ComponentProps) { + return new Promise | null>(async (resolve, reject) => { + const modal = await this.modalController.create({ + component, + componentProps, + }); + + modal.onWillDismiss().then((ev) => resolve(ev.data ?? null)); + + await modal.present(); + }); + } } diff --git a/frontend/src/app/shared/components/edit-button/edit-button.component.html b/frontend/src/app/shared/components/edit-button/edit-button.component.html index 0757f6e54..dcbae303c 100644 --- a/frontend/src/app/shared/components/edit-button/edit-button.component.html +++ b/frontend/src/app/shared/components/edit-button/edit-button.component.html @@ -1,4 +1,4 @@ - + {{ label }} diff --git a/frontend/src/app/shared/components/edit-button/edit-button.component.ts b/frontend/src/app/shared/components/edit-button/edit-button.component.ts index 227e0de27..36b5ea922 100644 --- a/frontend/src/app/shared/components/edit-button/edit-button.component.ts +++ b/frontend/src/app/shared/components/edit-button/edit-button.component.ts @@ -7,4 +7,5 @@ import { Component, Input } from "@angular/core"; }) export class EditButtonComponent { @Input() label?: string; + @Input() disabled?: boolean; } diff --git a/frontend/src/app/shared/components/item/item.component.html b/frontend/src/app/shared/components/item/item.component.html new file mode 100644 index 000000000..385493f9d --- /dev/null +++ b/frontend/src/app/shared/components/item/item.component.html @@ -0,0 +1,8 @@ + + {{ label }} + + + + + + diff --git a/frontend/src/app/shared/components/item/item.component.scss b/frontend/src/app/shared/components/item/item.component.scss new file mode 100644 index 000000000..c99cd81a3 --- /dev/null +++ b/frontend/src/app/shared/components/item/item.component.scss @@ -0,0 +1,4 @@ +::ng-deep p { + margin: 10px 0px; + flex: 1; +} diff --git a/frontend/src/app/shared/components/item/item.component.ts b/frontend/src/app/shared/components/item/item.component.ts new file mode 100644 index 000000000..fcb648561 --- /dev/null +++ b/frontend/src/app/shared/components/item/item.component.ts @@ -0,0 +1,15 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +@Component({ + selector: "bo-item", + templateUrl: "./item.component.html", + styleUrl: "./item.component.scss", +}) +export class ItemComponent { + @Input() label?: string; + @Input() editable: boolean = false; + @Input() loading?: boolean; + @Input() lines?: string; + + @Output() edit = new EventEmitter(); +} diff --git a/frontend/src/app/shared/components/tab/tab.component.html b/frontend/src/app/shared/components/tab/tab.component.html new file mode 100644 index 000000000..826054db0 --- /dev/null +++ b/frontend/src/app/shared/components/tab/tab.component.html @@ -0,0 +1,5 @@ + + + {{ label }} + {{ badge || "  " }} + diff --git a/frontend/src/app/shared/components/tab/tab.component.scss b/frontend/src/app/shared/components/tab/tab.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/shared/components/tab/tab.component.ts b/frontend/src/app/shared/components/tab/tab.component.ts new file mode 100644 index 000000000..0a485e882 --- /dev/null +++ b/frontend/src/app/shared/components/tab/tab.component.ts @@ -0,0 +1,41 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { TABS_QUERY_PARAM } from "../tabs/tabs.component"; + +@Component({ + selector: "bo-tab", + templateUrl: "./tab.component.html", + styleUrl: "./tab.component.scss", +}) +export class TabComponent implements OnInit { + @Input() label?: string; + @Input() name?: string; + @Input() icon?: string; + @Input() color?: string; + @Input() disabled?: boolean; + + @Input() badge?: string | number; + @Input() badgeColor?: string; + + active = false; + + constructor( + private readonly route: ActivatedRoute, + private readonly router: Router, + ) {} + + ngOnInit(): void { + this.route.queryParams.subscribe((params) => { + this.active = params[TABS_QUERY_PARAM] === this.name; + }); + } + + openTab() { + const queryParams = this.route.snapshot.queryParams; + this.router.navigate([], { + relativeTo: this.route, + queryParams: { ...queryParams, [TABS_QUERY_PARAM]: this.name }, + replaceUrl: true, + }); + } +} diff --git a/frontend/src/app/shared/components/tabs/tabs.component.html b/frontend/src/app/shared/components/tabs/tabs.component.html new file mode 100644 index 000000000..b511ed805 --- /dev/null +++ b/frontend/src/app/shared/components/tabs/tabs.component.html @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/app/shared/components/tabs/tabs.component.scss b/frontend/src/app/shared/components/tabs/tabs.component.scss new file mode 100644 index 000000000..89f3b27d2 --- /dev/null +++ b/frontend/src/app/shared/components/tabs/tabs.component.scss @@ -0,0 +1,3 @@ +ion-tab-bar { + justify-content: space-evenly; +} diff --git a/frontend/src/app/shared/components/tabs/tabs.component.ts b/frontend/src/app/shared/components/tabs/tabs.component.ts new file mode 100644 index 000000000..a81ff0e0c --- /dev/null +++ b/frontend/src/app/shared/components/tabs/tabs.component.ts @@ -0,0 +1,38 @@ +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; + +export const TABS_QUERY_PARAM = "tab"; + +@Component({ + selector: "bo-tabs", + templateUrl: "./tabs.component.html", + styleUrl: "./tabs.component.scss", + // providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TabsComponent), multi: true }], +}) +export class TabsComponent implements OnInit { + @Input() defaultTab?: string; + + @Output() change = new EventEmitter(); + + constructor( + private router: Router, + private route: ActivatedRoute, + ) {} + + ngOnInit(): void { + this.route.queryParams.subscribe((params) => { + const name = params[TABS_QUERY_PARAM]; + + if (name) this.change.emit(name); + else if (this.defaultTab) this.openTab(this.defaultTab); + }); + } + + private openTab(name: string) { + this.router.navigate([], { + relativeTo: this.route, + queryParams: { tab: name }, + replaceUrl: true, + }); + } +} diff --git a/frontend/src/app/shared/pipes/date-range.pipe.ts b/frontend/src/app/shared/pipes/date-range.pipe.ts index a020054b0..38b236d6e 100644 --- a/frontend/src/app/shared/pipes/date-range.pipe.ts +++ b/frontend/src/app/shared/pipes/date-range.pipe.ts @@ -1,20 +1,39 @@ -import { Pipe, PipeTransform } from '@angular/core'; import { formatDate } from "@angular/common"; +import { Pipe, PipeTransform } from "@angular/core"; // (value: string | number | Date, format: string, locale: string, timezone?: string): string @Pipe({ - name: 'dateRange' + name: "dateRange", }) export class DateRangePipe implements PipeTransform { + transform( + value: [string | Date | undefined | null, string | Date | undefined | null], + format1: string = "d. M. y", + format2: string = "d. M.", + format3: string = "d.", + separator: string = " – ", + ): string { + if (value[0] && value[1]) { + let dateFrom = new Date(value[0]); + let dateTill = new Date(value[1]); - transform(value:Array, format1:string = "d. M. y", format2:string = "d. M.", format3:string = "d.", separator:string = " – "):string { - let dateFrom = new Date(value[0]); - let dateTill = new Date(value[1]); - - if(dateFrom.getFullYear() !== dateTill.getFullYear()) return formatDate(dateFrom,format1,"cs") + separator + formatDate(dateTill,format1,"cs"); - if(dateFrom.getMonth() !== dateTill.getMonth()) return formatDate(dateFrom,format2,"cs") + separator + formatDate(dateTill,format1,"cs"); - if(dateFrom.getDate() !== dateTill.getDate()) return formatDate(dateFrom,format3,"cs") + separator + formatDate(dateTill,format1,"cs"); - return formatDate(dateFrom,format1,"cs"); - } + if (dateFrom.getFullYear() !== dateTill.getFullYear()) + return formatDate(dateFrom, format1, "cs") + separator + formatDate(dateTill, format1, "cs"); + if (dateFrom.getMonth() !== dateTill.getMonth()) + return formatDate(dateFrom, format2, "cs") + separator + formatDate(dateTill, format1, "cs"); + if (dateFrom.getDate() !== dateTill.getDate()) + return formatDate(dateFrom, format3, "cs") + separator + formatDate(dateTill, format1, "cs"); + return formatDate(dateFrom, format1, "cs"); + } + + if (value[0]) { + return "?" + separator + formatDate(new Date(value[0]), format1, "cs"); + } + if (value[1]) { + return formatDate(new Date(value[1]), format1, "cs") + separator + "?"; + } + + return "?" + separator + "?"; + } } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 482efb5cb..eed2bd0f8 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -41,6 +41,9 @@ import { CardHeaderComponent } from "./components/card-header/card-header.compon import { CardOpenButtonComponent } from "./components/card-open-button/card-open-button.component"; import { CopyButtonComponent } from "./components/copy-button/copy-button.component"; import { GroupBadgeComponent } from "./components/group-badge/group-badge.component"; +import { ItemComponent } from "./components/item/item.component"; +import { TabComponent } from "./components/tab/tab.component"; +import { TabsComponent } from "./components/tabs/tabs.component"; // register Swiper custom elements register(); @@ -83,6 +86,9 @@ register(); GroupBadgeComponent, CardOpenButtonComponent, CardHeaderComponent, + TabsComponent, + TabComponent, + ItemComponent, ], exports: [ FormsModule, @@ -125,6 +131,9 @@ register(); GroupBadgeComponent, CardOpenButtonComponent, CardHeaderComponent, + TabsComponent, + TabComponent, + ItemComponent, ], providers: [DatePipe], })