Skip to content

Commit

Permalink
feat: wip: event view
Browse files Browse the repository at this point in the history
  • Loading branch information
Martin Kopeček committed Feb 6, 2024
1 parent 4aaa397 commit 016e7ad
Show file tree
Hide file tree
Showing 53 changed files with 898 additions and 722 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<h3>Účtování</h3>
<bo-event-expenses-chart></bo-event-expenses-chart>
<ion-list>
<ion-item-sliding #sliding *ngFor="let expense of expenses" (click)="toggleSliding(sliding)">
<ion-item [button]="true" [detail]="false">
<ion-label>
<h2>
<strong>{{ expense.id }}</strong> - {{ expense.description }}
<!-- TODO: <ion-icon class="ms-1" *ngIf="expense.photo" name="camera-outline"></ion-icon> -->
</h2>
<p class="d-lg-none">{{ expense.amount | number: "1.2-2" : "cs" }}&nbsp;Kč</p>
</ion-label>

<ion-badge mode="ios" [color]="getExpenseColor(expense)">{{ expense.type }}</ion-badge>

<ion-label class="d-none d-lg-block text-right me-3">
{{ expense.amount | number: "1.2-2" : "cs" }}&nbsp;Kč
</ion-label>
</ion-item>
<ion-item-options side="end">
<ion-item-option color="primary" (click)="editExpenseModal(expense)">
<ion-icon slot="icon-only" name="create-outline"></ion-icon>
</ion-item-option>
<ion-item-option color="danger" (click)="removeExpense(expense)">
<ion-icon slot="icon-only" name="trash-outline"></ion-icon>
</ion-item-option>
</ion-item-options>
</ion-item-sliding>
</ion-list>
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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[] = [];

Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<h3>Účastníci</h3>

<ion-list>
<!-- ATTENDEES -->
<p *ngIf="attendees && !attendees.length"><em>Žádní účastníci</em></p>

<ion-item *ngFor="let attendee of attendees">
<ion-label>
<h3>{{ attendee.member | member: "nickname" }}</h3>
<p><bo-member-item-detail *ngIf="attendee.member" [member]="attendee.member"></bo-member-item-detail></p>
</ion-label>
<ion-badge
slot="end"
mode="ios"
[style.background-color]="attendee.member?.groupId | group: 'color'"
class="d-none d-xl-block"
>
{{ attendee.member?.groupId | group: "name" }}
</ion-badge>
<ion-button fill="clear" [routerLink]="'/databaze/' + attendee.memberId" slot="end">
<ion-icon name="information-circle-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" color="danger" (click)="removeAttendee(attendee)" slot="end">
<ion-icon name="trash-outline"></ion-icon>
</ion-button>
</ion-item>

<div class="text-center">
<bo-add-button (click)="addAttendee()">Přidat účastníka</bo-add-button>
</div>
</ion-list>
Original file line number Diff line number Diff line change
@@ -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<void>();

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!),
},
];
}
}
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -8,15 +8,29 @@ 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;

total: number = 0;

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 })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<h3>Základní informace</h3>

<ion-list>
<bo-item label="Název" [loading]="!event" [editable]="!!event?._links?.updateEvent?.allowed" (edit)="editName()">
<p>{{ event?.name }}</p>
</bo-item>

<bo-item label="Termín" [loading]="!event" [editable]="!!event?._links?.updateEvent?.allowed" (edit)="editDate()">
<p>{{ [event?.dateFrom, event?.dateTill] | dateRange }}</p>
</bo-item>

<bo-item label="Typ" [loading]="!event">
<bo-event-type-selector
*ngIf="event"
[disabled]="!event._links.updateEvent.allowed"
[ngModel]="event.type"
(ngModelChange)="updateType($event)"
slot="end"
></bo-event-type-selector>
</bo-item>
</ion-list>

<h3 class="mt-5">Popis</h3>
<bo-card>
<bo-card-content>
<bo-item
lines="none"
[loading]="!event"
[editable]="!!event?._links?.updateEvent?.allowed"
(edit)="editDescription()"
>
<p *ngIf="event">{{ event.description }}</p>
</bo-item>
</bo-card-content>
</bo-card>
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
:host {
display: block;
}

.attendees {
ion-chip {
color: #fff;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Partial<EventResponse>>();

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 });
}
}
Loading

0 comments on commit 016e7ad

Please sign in to comment.