refactor admin to use eventbus

This commit is contained in:
Roland Schneider
2025-12-11 22:47:54 +01:00
parent 93b1fb9610
commit 453d02612c
27 changed files with 298 additions and 225 deletions

View File

@@ -1,23 +1,19 @@
<div> <div>
<h1>Naptár</h1> <h1>Naptár</h1>
<!-- <div>-->
<!-- <input type="text" name="startHour" id="starthour" #startHour>-->
<!-- <a class="btn" (click)="addEvent($event)">add</a>-->
<!-- </div>-->
<full-calendar #calendar [options]="calendarOptions"></full-calendar> <full-calendar #calendar [options]="calendarOptions"></full-calendar>
</div> </div>
@if (workflow() == 'event-create') { @if (workflow() == 'event_create') {
<rs-daisy-modal [isOpen]="true" (closeClick)="closeDialog()" [modalBoxStyleClass]="'max-w-none w-2xl'"> <rs-daisy-modal [isOpen]="true" (closeClick)="closeDialog()" [modalBoxStyleClass]="'max-w-none w-2xl'">
<app-create-event-form (ready)="closeDialog()" [date]="selectedDate()" <app-create-event-form (ready)="closeDialog()" [date]="selectedDate()"
[id]="undefined"></app-create-event-form> [id]="undefined"></app-create-event-form>
</rs-daisy-modal> </rs-daisy-modal>
} }
@if (workflow() == 'event-dashboard' && selectedEvent()) { @if (workflow() == 'event_dashboard' && selectedEvent()) {
<rs-daisy-modal [isOpen]="true" (closeClick)="closeDialog()" [modalBoxStyleClass]="'max-w-none w-2xl'"> <rs-daisy-modal [isOpen]="true" (closeClick)="closeDialog()" [modalBoxStyleClass]="'max-w-none w-2xl'">
<app-single-event-dashboard [event]="selectedEvent()" <app-single-event-dashboard [event]="selectedEvent()"
(action)="onDashboardAction($event)" (action)="setWorkFlow($event)"
></app-single-event-dashboard> ></app-single-event-dashboard>
</rs-daisy-modal> </rs-daisy-modal>
} }

View File

@@ -27,6 +27,24 @@ import {
SingleEventDashboardEventActivation, SingleEventDashboardEventActivation,
} from './single-event-dashboard-event-activation/single-event-dashboard-event-activation.component'; } from './single-event-dashboard-event-activation/single-event-dashboard-event-activation.component';
import { SingleEventDashboardEventEdit } from './single-event-dashboard-event-edit/single-event-dashboard-event-edit'; import { SingleEventDashboardEventEdit } from './single-event-dashboard-event-edit/single-event-dashboard-event-edit';
import { EventBusService } from '../../../../services/event-bus.service';
import { EventType } from '../../../../../types';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { SingleEventBookingCreate } from './single-event-booking-create/single-event-booking-create';
import { SingleEventBookingList } from './single-event-booking-list/single-event-booking-list';
export type WORKFLOW_TYPE = 'NO_DIALOG'
| 'event_delete'
| 'event_cancel'
| 'event_dashboard'
| 'event_create'
| 'event_activate'
| 'event_edit'
| 'booking_create'
| 'booking_list'
| 'booking_cancel'
;
@Component({ @Component({
selector: 'app-calendar-view', selector: 'app-calendar-view',
@@ -34,14 +52,16 @@ import { SingleEventDashboardEventEdit } from './single-event-dashboard-event-ed
templateUrl: './calendar-view.html', templateUrl: './calendar-view.html',
styleUrl: './calendar-view.css', styleUrl: './calendar-view.css',
}) })
export class CalendarView { export class CalendarView {
@ViewChild('startHour') startHour!: ElementRef; @ViewChild('startHour') startHour!: ElementRef;
@ViewChild('calendar') calendarComponent: FullCalendarComponent | undefined; @ViewChild('calendar') calendarComponent: FullCalendarComponent | undefined;
private eventBus = inject(EventBusService);
calendarService = inject(CalendarService); calendarService = inject(CalendarService);
workflow = signal<string>(''); workflow = signal<WORKFLOW_TYPE>('NO_DIALOG');
selectedDate = signal<Date | undefined>(undefined); selectedDate = signal<Date | undefined>(undefined);
selectedEvent = signal<CalendarEventDto | undefined>(undefined); selectedEvent = signal<CalendarEventDto | undefined>(undefined);
@@ -50,6 +70,24 @@ export class CalendarView {
constructor() { constructor() {
this.eventBus.on(EventType.CALENDAR_VIEW_EVENT_SAVED)
.pipe(takeUntilDestroyed())
.subscribe({
next: (_) => {
this.closeDialog();
this.calendarComponent?.getApi().refetchEvents();
}
});
this.eventBus.on(EventType.CALENDAR_VIEW_DIALOG_CLOSED)
.pipe(takeUntilDestroyed())
.subscribe({
next: (_) => {
this.closeDialog();
}
})
this.calendarOptions = { this.calendarOptions = {
plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin], plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin],
initialView: 'dayGridMonth', initialView: 'dayGridMonth',
@@ -65,12 +103,12 @@ export class CalendarView {
eventClick: (info) => { eventClick: (info) => {
this.selectedEvent.set(info.event.extendedProps['event']); this.selectedEvent.set(info.event.extendedProps['event']);
this.workflow.set('event-dashboard'); this.workflow.set('event_dashboard');
this.selectedDate.set(undefined); this.selectedDate.set(undefined);
}, },
dateClick: (info) => { dateClick: (info) => {
this.workflow.set('event-create'); this.workflow.set('event_create');
this.selectedDate.set(info.date); this.selectedDate.set(info.date);
this.selectedEvent.set(undefined); this.selectedEvent.set(undefined);
}, },
@@ -80,13 +118,12 @@ export class CalendarView {
this.dialogs = [ this.dialogs = [
{ {
component: SingleEventDashboardEventDelete, component: SingleEventDashboardEventDelete,
isRendered: () => this.workflow() == 'event-delete', isRendered: () => this.workflow() == 'event_delete',
closeClick: () => this.closeDialog(), closeClick: () => this.closeDialog(),
modalBoxStyleClass: 'max-w-none w-2xl', modalBoxStyleClass: 'max-w-none w-2xl',
componentInputs: () => { componentInputs: () => {
return { return {
'event': this.selectedEvent(), 'event': this.selectedEvent(),
'onAction': this.handleAction,
}; };
}, },
@@ -94,14 +131,13 @@ export class CalendarView {
{ {
component: SingleEventDashboardEventActivation, component: SingleEventDashboardEventActivation,
isRendered: () => this.workflow() == 'event-cancel', isRendered: () => this.workflow() == 'event_cancel',
closeClick: () => this.closeDialog(), closeClick: () => this.closeDialog(),
modalBoxStyleClass: 'max-w-none w-2xl', modalBoxStyleClass: 'max-w-none w-2xl',
componentInputs: () => { componentInputs: () => {
return { return {
'mode': 'cancel', 'mode': 'cancel',
'event': this.selectedEvent(), 'event': this.selectedEvent(),
'onAction': this.handleAction,
}; };
}, },
}, },
@@ -114,26 +150,61 @@ export class CalendarView {
return { return {
'mode': 'activate', 'mode': 'activate',
'event': this.selectedEvent(), 'event': this.selectedEvent(),
'onAction': this.handleAction,
}; };
}, },
}, },
{ {
component: SingleEventDashboardEventEdit, component: SingleEventDashboardEventEdit,
isRendered: () => this.workflow() == 'event-edit', isRendered: () => this.workflow() == 'event_edit',
// isRendered: () => true,
closeClick: () => this.closeDialog(),
modalBoxStyleClass: 'max-w-none w-2xl',
componentInputs: () => {
return {
'event': this.selectedEvent(),
};
},
},
{
component: SingleEventBookingCreate,
isRendered: () => this.workflow() == 'booking_create',
// isRendered: () => true,
closeClick: () => this.closeDialog(),
modalBoxStyleClass: 'max-w-none w-2xl',
componentInputs: () => {
return {
'event': this.selectedEvent(),
};
},
},
{
component: SingleEventBookingList,
isRendered: () => this.workflow() == 'booking_list',
// isRendered: () => true,
closeClick: () => this.closeDialog(),
modalBoxStyleClass: 'max-w-none w-2xl',
componentInputs: () => {
return {
'event': this.selectedEvent(),
};
},
},
{
component: SingleEventBookingList,
isRendered: () => this.workflow() == 'booking_cancel',
// isRendered: () => true, // isRendered: () => true,
closeClick: () => this.closeDialog(), closeClick: () => this.closeDialog(),
modalBoxStyleClass: 'max-w-none w-2xl', modalBoxStyleClass: 'max-w-none w-2xl',
componentInputs: () => { componentInputs: () => {
return { return {
'event': this.selectedEvent(), 'event': this.selectedEvent(),
'onAction': this.handleAction,
}; };
}, },
}, },
]; ];
} }
fetchEvents(fetchInfo: any, successCallback: (events: EventInput[]) => void, failureCallback: (error: any) => void): void { fetchEvents(fetchInfo: any, successCallback: (events: EventInput[]) => void, failureCallback: (error: any) => void): void {
console.info('fetching events', fetchInfo); console.info('fetching events', fetchInfo);
@@ -183,58 +254,21 @@ export class CalendarView {
); );
} }
// protected addEvent($event: PointerEvent) {
// let hourStr = this.startHour.nativeElement.value;
// const hour = parseInt(hourStr, 10);
// const date = new Date();
// const start = new Date(date.getTime());
// start.setHours(hour, 0, 0);
// const end = new Date(date.getTime());
// // end.setHours(3,0,0)
//
// this.calendarComponent?.getApi().addEvent({
// title: 'Event at ' + hour,
// start,
// });
// }
closeDialog() { closeDialog() {
this.workflow.set(''); this.workflow.set('NO_DIALOG');
} }
onDashboardAction(action: string) { /**
console.info('dashboard event', action); * Set dashboard workflow
switch (action) { * @param workflowType
case 'event_delete': */
this.workflow.set('event-delete'); setWorkFlow(workflowType: WORKFLOW_TYPE) {
break; console.info('set workflow to', workflowType);
this.workflow.set(workflowType);
case 'event_cancel':
this.workflow.set('event-cancel');
break;
case 'event_edit':
this.workflow.set('event-edit');
break;
default:
this.workflow.set(action);
break;
}
} }
// This function is passed into the child
handleAction = (msg: string) => {
console.log('Parent received:', msg);
if (msg == 'close') {
this.closeDialog();
} else if ( msg == 'save-event-success'){
this.closeDialog();
this.calendarComponent?.getApi().refetchEvents();
}else{
}
};
} }
export interface DialogConfig { export interface DialogConfig {

View File

@@ -0,0 +1 @@
<p>single-event-booking-cancel works!</p>

View File

@@ -0,0 +1,14 @@
import { Component, inject, input } from '@angular/core';
import { EventBusService } from '../../../../../services/event-bus.service';
import { CalendarEventDto } from '../../../models/events-in-range-dto.model';
@Component({
selector: 'app-single-event-booking-cancel',
imports: [],
templateUrl: './single-event-booking-cancel.html',
styleUrl: './single-event-booking-cancel.css',
})
export class SingleEventBookingCancel {
eventBus = inject(EventBusService);
event = input<CalendarEventDto>();
}

View File

@@ -0,0 +1 @@
<p>single-event-booking-create works!</p>

View File

@@ -0,0 +1,15 @@
import { Component, inject, input } from '@angular/core';
import { CalendarEventDto } from '../../../models/events-in-range-dto.model';
import { EventBusService } from '../../../../../services/event-bus.service';
@Component({
selector: 'app-single-event-booking-create',
imports: [],
templateUrl: './single-event-booking-create.html',
styleUrl: './single-event-booking-create.css',
})
export class SingleEventBookingCreate {
eventBus = inject(EventBusService);
event = input<CalendarEventDto>();
}

View File

@@ -0,0 +1 @@
<p>single-event-booking-list works!</p>

View File

@@ -0,0 +1,14 @@
import { Component, inject, input } from '@angular/core';
import { EventBusService } from '../../../../../services/event-bus.service';
import { CalendarEventDto } from '../../../models/events-in-range-dto.model';
@Component({
selector: 'app-single-event-booking-list',
imports: [],
templateUrl: './single-event-booking-list.html',
styleUrl: './single-event-booking-list.css',
})
export class SingleEventBookingList {
eventBus = inject(EventBusService);
event = input<CalendarEventDto>();
}

View File

@@ -1,5 +1,5 @@
import { Component, inject, input } from '@angular/core'; import { Component, inject, input } from '@angular/core';
import { CalendarEventDto, EventExceptionDto } from '../../../models/events-in-range-dto.model'; import { CalendarEventDto } from '../../../models/events-in-range-dto.model';
import { import {
SingleEventDashboardEventDetailsView, SingleEventDashboardEventDetailsView,
} from '../single-event-dashboard-event-details-view/single-event-dashboard-event-details-view'; } from '../single-event-dashboard-event-details-view/single-event-dashboard-event-details-view';
@@ -8,6 +8,8 @@ import { SvgIcons } from '../../../../../svg-icons';
import { SafeHtmlPipe } from '../../../../../pipes/safe-html-pipe'; import { SafeHtmlPipe } from '../../../../../pipes/safe-html-pipe';
import { CalendarService } from '../../../services/calendar.service'; import { CalendarService } from '../../../services/calendar.service';
import { CreateExceptionDto } from '../../../models/event-exception.model'; import { CreateExceptionDto } from '../../../models/event-exception.model';
import { EventBusService } from '../../../../../services/event-bus.service';
import { EventType } from '../../../../../../types';
export type ACTIVATION_TYPE = 'cancel' | 'activate'; export type ACTIVATION_TYPE = 'cancel' | 'activate';
@@ -23,11 +25,10 @@ export type ACTIVATION_TYPE = 'cancel' | 'activate';
}) })
export class SingleEventDashboardEventActivation { export class SingleEventDashboardEventActivation {
eventBus = inject(EventBusService);
mode = input<ACTIVATION_TYPE>('cancel'); mode = input<ACTIVATION_TYPE>('cancel');
calendarService = inject(CalendarService); calendarService = inject(CalendarService);
event = input<CalendarEventDto>(); event = input<CalendarEventDto>();
onAction = input.required<(msg: string) => void>();
protected readonly SvgIcons = SvgIcons; protected readonly SvgIcons = SvgIcons;
@@ -37,7 +38,7 @@ export class SingleEventDashboardEventActivation {
console.info('setEventOccurrenceActivation', event); console.info('setEventOccurrenceActivation', event);
const eventId = this.event()?.id!; const eventId = this.event()?.id!;
const startTime = this.event()?.startTime!; const startTime = this.event()?.startTime!;
let payload: CreateExceptionDto | undefined = undefined; let payload: CreateExceptionDto | undefined;
const eventException = event?.exceptions?.length ? event.exceptions[0] : undefined; const eventException = event?.exceptions?.length ? event.exceptions[0] : undefined;
if (eventException) { if (eventException) {
payload = { payload = {
@@ -57,9 +58,10 @@ export class SingleEventDashboardEventActivation {
this.calendarService.applyException(eventId, payload ).subscribe( this.calendarService.applyException(eventId, payload ).subscribe(
{ {
next: () => { next: () => {
this.onAction()('save-event-success'); this.eventBus.emit(EventType.CALENDAR_VIEW_EVENT_SAVED, 'Event saved')
}, },
error: err => { error: err => {
console.error(err);
alert('Failed to change event'); alert('Failed to change event');
}, },
}, },
@@ -75,7 +77,7 @@ export class SingleEventDashboardEventActivation {
} }
protected closeDialog() { protected closeDialog() {
this.onAction()('close'); this.eventBus.emit(EventType.CALENDAR_VIEW_DIALOG_CLOSED, 'Event saved')
} }
} }

View File

@@ -1,17 +1,14 @@
<h2 class="text-2xl">Törlés</h2> <h2 class="text-2xl">Törlés</h2>
<p>Esemény és az egész széria törlése</p> <p>Esemény és az egész széria törlése</p>
@if (config) { <app-single-event-dashboard-event-details-view [event]="event()">
<app-detail-view </app-single-event-dashboard-event-details-view>
[config]="config"
></app-detail-view>
}
<div class="flex gap-2 mt-3"> <div class="flex gap-2 mt-3">
<rs-daisy-button variant="error" (click)="doDelete()"> <rs-daisy-button variant="error" (click)="deleteEventTemplate()">
<span [outerHTML]="SvgIcons.heroTrash | safeHtml"></span> <span [outerHTML]="SvgIcons.heroTrash | safeHtml"></span>
Törlés Törlés
</rs-daisy-button> </rs-daisy-button>
<rs-daisy-button variant="primary" (click)="triggerAction()"> <rs-daisy-button variant="primary" (click)="closeDialog()">
<span [outerHTML]="SvgIcons.heroXcircle | safeHtml"></span> <span [outerHTML]="SvgIcons.heroXcircle | safeHtml"></span>
Mégsem Mégsem
</rs-daisy-button> </rs-daisy-button>

View File

@@ -1,76 +1,41 @@
import { Component, effect, inject, input, output } from '@angular/core'; import { Component, inject, input } from '@angular/core';
import { CalendarEventDto } from '../../../models/events-in-range-dto.model'; import { CalendarEventDto } from '../../../models/events-in-range-dto.model';
import { DetailView, DetailViewConfig } from '../../../../../components/detail-view/detail-view';
import { SvgIcons } from '../../../../../svg-icons'; import { SvgIcons } from '../../../../../svg-icons';
import { SafeHtmlPipe } from '../../../../../pipes/safe-html-pipe'; import { SafeHtmlPipe } from '../../../../../pipes/safe-html-pipe';
import { Button } from '@rschneider/ng-daisyui'; import { Button } from '@rschneider/ng-daisyui';
import { CalendarService } from '../../../services/calendar.service'; import { CalendarService } from '../../../services/calendar.service';
import { EventBusService } from '../../../../../services/event-bus.service';
import {
SingleEventDashboardEventDetailsView
} from '../single-event-dashboard-event-details-view/single-event-dashboard-event-details-view';
import { EventType } from '../../../../../../types';
@Component({ @Component({
selector: 'app-single-event-dashboard-event-delete', selector: 'app-single-event-dashboard-event-delete',
imports: [ imports: [
DetailView,
SafeHtmlPipe, SafeHtmlPipe,
Button, Button,
SingleEventDashboardEventDetailsView,
], ],
templateUrl: './single-event-dashboard-event-delete.html', templateUrl: './single-event-dashboard-event-delete.html',
styleUrl: './single-event-dashboard-event-delete.css', styleUrl: './single-event-dashboard-event-delete.css',
}) })
export class SingleEventDashboardEventDelete { export class SingleEventDashboardEventDelete {
eventBus = inject(EventBusService);
calendarService = inject(CalendarService); calendarService = inject(CalendarService);
event = input<CalendarEventDto>(); event = input<CalendarEventDto>();
// Define an input that expects a function
onAction = input.required<(msg: string) => void>();
config: DetailViewConfig<CalendarEventDto> | undefined;
constructor() {
effect(() => {
this.config = {
data: this.event()!,
rows: [{
attribute: 'id',
getTitle: 'Esemény azonosító',
},
{
attribute: 'title',
getTitle: 'Esemény neve',
},
{
attribute: 'startTime',
getTitle: 'Kezdési időpont',
format: 'datetime',
},
{
attribute: 'eventType',
getTitle: 'Esemény típusa',
getValue: obj => obj.eventType.name,
},
],
};
});
}
triggerAction() {
// Call the function passed from the parent
this.onAction()('close');
}
doDelete() {
this.calendarService.deleteEvent(this.event()!.id).subscribe(
{
next: ( ) => {
this.onAction()('save-event-success');
}
}
)
// Call the function passed from the parent
}
protected readonly SvgIcons = SvgIcons; protected readonly SvgIcons = SvgIcons;
closeDialog() {
// Call the function passed from the parent
this.eventBus.emit(EventType.CALENDAR_VIEW_DIALOG_CLOSED, '')
}
protected deleteEventTemplate() {
this.calendarService.delete(this.event()!.id).subscribe(() => this.eventBus.emit(EventType.CALENDAR_VIEW_EVENT_SAVED, 'Event saved'))
}
} }

View File

@@ -1,3 +1,5 @@
<app-create-event-form (ready)="triggerAction($event)" [date]="selectedDate()" <app-create-event-form
(ready)="triggerAction($event)"
[id]="event()?.id"></app-create-event-form> [date]="selectedDate()"
[id]="event()?.id">
</app-create-event-form>

View File

@@ -1,31 +1,33 @@
import { Component, input, signal } from '@angular/core'; import { Component, inject, input, signal } from '@angular/core';
import { CalendarEventDto } from '../../../models/events-in-range-dto.model'; import { CalendarEventDto } from '../../../models/events-in-range-dto.model';
import { SvgIcons } from '../../../../../svg-icons'; import { CreateEventForm, ReadyType } from '../../create-event-form/create-event-form';
import { CreateEventForm } from '../../create-event-form/create-event-form'; import { EventBusService } from '../../../../../services/event-bus.service';
import { Modal } from '@rschneider/ng-daisyui'; import { EventType } from '../../../../../../types';
@Component({ @Component({
selector: 'app-single-event-dashboard-event-edit', selector: 'app-single-event-dashboard-event-edit',
imports: [ imports: [
CreateEventForm, CreateEventForm,
Modal,
], ],
templateUrl: './single-event-dashboard-event-edit.html', templateUrl: './single-event-dashboard-event-edit.html',
styleUrl: './single-event-dashboard-event-edit.css', styleUrl: './single-event-dashboard-event-edit.css',
}) })
export class SingleEventDashboardEventEdit { export class SingleEventDashboardEventEdit {
eventBus = inject(EventBusService);
selectedDate = signal<Date | undefined>(undefined); selectedDate = signal<Date | undefined>(undefined);
event = input<CalendarEventDto>(); event = input<CalendarEventDto>();
onAction = input.required<(msg: string) => void>();
protected readonly SvgIcons = SvgIcons;
/** /**
* proxy to ready event from form to parent * proxy to ready event from form to parent
*/ */
protected triggerAction(action: string) { protected triggerAction(action: ReadyType) {
console.info("event details dashboard", action) if ( action == 'save-event-success'){
this.onAction()(action) this.eventBus.emit(EventType.CALENDAR_VIEW_EVENT_SAVED,'');
}else if ( action == 'save-event-failed'){
this.eventBus.emit(EventType.CALENDAR_VIEW_DIALOG_CLOSED,'');
}else{
this.eventBus.emit(EventType.CALENDAR_VIEW_DIALOG_CLOSED,'');
}
} }
} }

View File

@@ -4,11 +4,8 @@
@if (!event()?.isRecurring) { @if (!event()?.isRecurring) {
<h2 class="text-xl">Esemény </h2> <h2 class="text-xl">Esemény </h2>
} }
@if (config) { <app-single-event-dashboard-event-details-view [event]="event()">
<app-detail-view </app-single-event-dashboard-event-details-view>
[config]="config"
></app-detail-view>
}
<div class="flex mt-3 gap-2 flex-wrap content-stretch "> <div class="flex mt-3 gap-2 flex-wrap content-stretch ">
@for (card of cards(); let i = $index; track i) { @for (card of cards(); let i = $index; track i) {
@@ -19,7 +16,7 @@
[description]="card.description" [description]="card.description"
[buttonTitle]="card.buttonTitle" [buttonTitle]="card.buttonTitle"
[styleClass]="'flex-1'" [styleClass]="'flex-1'"
(onAction)="onCardAction(card.action)" (onAction)="onCardAction(card.workflow)"
></app-single-event-dashboard-card> ></app-single-event-dashboard-card>
} }

View File

@@ -3,12 +3,17 @@ import { CalendarEventDto } from '../../../models/events-in-range-dto.model';
import { DetailView, DetailViewConfig } from '../../../../../components/detail-view/detail-view'; import { DetailView, DetailViewConfig } from '../../../../../components/detail-view/detail-view';
import { SvgIcons } from '../../../../../svg-icons'; import { SvgIcons } from '../../../../../svg-icons';
import { SingleEventDashboardCard } from '../single-event-dashboard-card/single-event-dashboard-card'; import { SingleEventDashboardCard } from '../single-event-dashboard-card/single-event-dashboard-card';
import { WORKFLOW_TYPE } from '../calendar-view';
import {
SingleEventDashboardEventDetailsView
} from '../single-event-dashboard-event-details-view/single-event-dashboard-event-details-view';
@Component({ @Component({
selector: 'app-single-event-dashboard', selector: 'app-single-event-dashboard',
imports: [ imports: [
DetailView, DetailView,
SingleEventDashboardCard, SingleEventDashboardCard,
SingleEventDashboardEventDetailsView,
], ],
templateUrl: './single-event-dashboard.html', templateUrl: './single-event-dashboard.html',
styleUrl: './single-event-dashboard.css', styleUrl: './single-event-dashboard.css',
@@ -16,49 +21,20 @@ import { SingleEventDashboardCard } from '../single-event-dashboard-card/single-
export class SingleEventDashboard { export class SingleEventDashboard {
event = input<CalendarEventDto>(); event = input<CalendarEventDto>();
action = output<string>(); action = output<WORKFLOW_TYPE>();
config: DetailViewConfig<CalendarEventDto> | undefined;
cards = signal<CardConfig[]>([]); cards = signal<CardConfig[]>([]);
constructor() { constructor() {
effect(() => { effect(() => {
console.info("dashboard", this.event());
this.config = {
data: this.event()!,
rows: [{
attribute: 'id',
getTitle: 'Esemény azonosító',
},
{
attribute: 'title',
getTitle: 'Esemény neve',
},
{
attribute: 'startTime',
getTitle: 'Kezdési időpont',
format: 'datetime',
},
{
attribute: 'eventType',
getTitle: 'Esemény típusa',
getValue: obj => obj.eventType.name,
},
],
};
this.cards.set( [ this.cards.set( [
{ {
buttonTitle: 'Szerkesztés', buttonTitle: 'Szerkesztés',
title: 'Szerkesztés', title: 'Szerkesztés',
svgIcon: SvgIcons.heorPencilSquare, svgIcon: SvgIcons.heorPencilSquare,
description: 'Az esemény módosítása', description: 'Az esemény módosítása',
action: 'event_edit', workflow: 'event_edit',
}, },
this.event()?.isCancelled ? this.event()?.isCancelled ?
@@ -68,48 +44,48 @@ export class SingleEventDashboard {
title: 'Előfordulás aktiválása', title: 'Előfordulás aktiválása',
svgIcon: SvgIcons.heroPlay, svgIcon: SvgIcons.heroPlay,
description: 'Az esemény ezen előfordulásának aktiválása', description: 'Az esemény ezen előfordulásának aktiválása',
action: 'event_activate', workflow: 'event_activate',
} : } :
{ {
buttonTitle: 'Lemondás', buttonTitle: 'Lemondás',
title: 'Előfordulás lemondása', title: 'Előfordulás lemondása',
svgIcon: SvgIcons.heroXcircle, svgIcon: SvgIcons.heroXcircle,
description: 'Az esemény ezen előfordulásának lemondása', description: 'Az esemény ezen előfordulásának lemondása',
action: 'event_cancel', workflow: 'event_cancel',
}, },
{ {
buttonTitle: 'Törlés', buttonTitle: 'Törlés',
title: 'Esemény törlése', title: 'Esemény törlése',
svgIcon: SvgIcons.heroTrash, svgIcon: SvgIcons.heroTrash,
description: 'Az esemény törlése', description: 'Az esemény törlése',
action: 'event_delete', workflow: 'event_delete',
}, },
{ {
buttonTitle: 'Megnézem', buttonTitle: 'Megnézem',
title: 'Foglalások', title: 'Foglalások',
svgIcon: SvgIcons.heroUserGroup, svgIcon: SvgIcons.heroUserGroup,
description: 'Foglalások megtekintése', description: 'Foglalások megtekintése',
action: 'event_bookings', workflow: 'booking_list',
}, },
{ {
buttonTitle: 'Bejelentkezés', buttonTitle: 'Bejelentkezés',
title: 'Időpont foglalás', title: 'Időpont foglalás',
svgIcon: SvgIcons.heroUserPlus, svgIcon: SvgIcons.heroUserPlus,
description: 'Időpont foglalása eseményre', description: 'Időpont foglalása eseményre',
action: 'user_booking', workflow: 'booking_create',
}, },
{ {
buttonTitle: 'Lemondás', buttonTitle: 'Lemondás',
title: 'Lemondás', title: 'Lemondás',
svgIcon: SvgIcons.heroUserMinus, svgIcon: SvgIcons.heroUserMinus,
description: 'Az időpont lemondása', description: 'Az időpont lemondása',
action: 'user_cancel', workflow: 'booking_cancel',
}, },
]); ]);
}); });
} }
onCardAction (action: string|undefined) { onCardAction (action: WORKFLOW_TYPE) {
if ( action ){ if ( action ){
this.action.emit(action); this.action.emit(action);
} }
@@ -125,5 +101,5 @@ export interface CardConfig {
description?: string; description?: string;
buttonTitle?: string; buttonTitle?: string;
svgIcon?: string; svgIcon?: string;
action?: string; workflow: WORKFLOW_TYPE;
} }

View File

@@ -16,6 +16,7 @@ export type FrequencyOption = {
frequency: Frequency, frequency: Frequency,
label: string; label: string;
} }
export type ReadyType = 'close' | 'save-event-success' | 'save-event-failed';
@Component({ @Component({
selector: 'app-create-event-form', selector: 'app-create-event-form',
@@ -28,7 +29,7 @@ export type FrequencyOption = {
export class CreateEventForm implements OnInit { export class CreateEventForm implements OnInit {
form: FormGroup; form: FormGroup;
isEditMode = false; isEditMode = false;
ready = output<string>(); ready = output<ReadyType>();
id = input<number | undefined>(); id = input<number | undefined>();
date = input<Date | undefined>(); date = input<Date | undefined>();
eventTypes = signal<EventType[]>([]); eventTypes = signal<EventType[]>([]);

View File

@@ -9,18 +9,17 @@ import { CreateExceptionDto } from '../models/event-exception.model';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root'
}) })
export class CalendarService { export class CalendarService {
private readonly apiUrl: string; private readonly apiUrl: string;
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private configService: ConfigurationService, private configService: ConfigurationService
) { ) {
this.apiUrl = `${this.configService.getApiUrl()}/calendar`; this.apiUrl = `${this.configService.getApiUrl()}/calendar`;
} }
/** /**
* get events in range * get events in range
*/ */
@@ -28,26 +27,24 @@ export class CalendarService {
const params = new HttpParams() const params = new HttpParams()
.set('startDate', eventsInRangeDto.startTime!.toISOString()) .set('startDate', eventsInRangeDto.startTime!.toISOString())
.set('endDate', eventsInRangeDto.endTime!.toISOString()); .set('endDate', eventsInRangeDto.endTime!.toISOString());
return this.http.get<CalendarEventDto[]>(this.apiUrl + '', { params }); return this.http.get<CalendarEventDto[]>(this.apiUrl+'', { params });
} }
/** /**
* Create a new record. * Create a new record.
*/ */
public create(data: EventFormDTO): Observable<Event> { public create(data: EventFormDTO): Observable<Event> {
return this.http.post<Event>(this.apiUrl + '/events', data); return this.http.post<Event>(this.apiUrl+'/events', data);
} }
public update(id: number, data: UpdateEventFormDTO): Observable<Event> { public update(id: number,data: UpdateEventFormDTO): Observable<Event> {
return this.http.patch<Event>(this.apiUrl + '/events/' + id, data); return this.http.patch<Event>(this.apiUrl+'/events/'+id, data);
} }
public applyException(eventId: number, eventException: CreateExceptionDto) { public applyException(eventId: number, eventException: CreateExceptionDto){
return this.http.post(this.apiUrl + `/events/${eventId}/exceptions`, eventException); return this.http.post(this.apiUrl+`/events/${eventId}/exceptions`, eventException);
} }
public delete(eventId: number){
public deleteEvent(id: number): Observable<void> { return this.http.delete(this.apiUrl+`/events/${eventId}` );
return this.http.delete<void>(this.apiUrl + '/events/' + id);
} }
} }

View File

@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { AppEvent, EventMap } from '../../types';
@Injectable({
providedIn: 'root' // Makes this a singleton available everywhere
})
export class EventBusService {
private subject$ = new Subject<AppEvent>();
/**
* Publish an event to the bus
*/
emit<K extends keyof EventMap>(type: K, payload: EventMap[K]): void {
this.subject$.next({ type, payload });
}
/**
* Subscribe to specific event types
* Returns an Observable of the payload
*/
on<K extends keyof EventMap>(type: K): Observable<EventMap[K]> {
return this.subject$.pipe(
filter(e => e.type === type),
map(e => e.payload)
);
}
}

View File

@@ -14,3 +14,24 @@ export interface PaginatedResponse<T> {
currentPage: number; currentPage: number;
}; };
} }
export enum EventType {
USER_LOGGED_IN = 'USER_LOGGED_IN',
ORDER_CREATED = 'ORDER_CREATED',
THEME_CHANGED = 'THEME_CHANGED',
CALENDAR_VIEW_EVENT_SAVED = 'CALENDAR_VIEW_EVENT_SAVED',
CALENDAR_VIEW_DIALOG_CLOSED = 'CALENDAR_VIEW_DIALOG_CLOSED'
}
export interface EventMap {
[EventType.USER_LOGGED_IN]: { id: number; name: string };
[EventType.THEME_CHANGED]: string;
[EventType.ORDER_CREATED]: { orderId: number };
[EventType.CALENDAR_VIEW_EVENT_SAVED]: string;
[EventType.CALENDAR_VIEW_DIALOG_CLOSED]: string;
}
export interface AppEvent<T = any> {
type: EventType;
payload?: T;
}

View File

@@ -16,7 +16,7 @@
"@nestjs/mapped-types": "^2.1.0", "@nestjs/mapped-types": "^2.1.0",
"@nestjs/passport": "^11.0.5", "@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.1", "@nestjs/swagger": "^11.2.3",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
@@ -2109,9 +2109,9 @@
} }
}, },
"node_modules/@microsoft/tsdoc": { "node_modules/@microsoft/tsdoc": {
"version": "0.15.1", "version": "0.16.0",
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz",
"integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
@@ -2620,17 +2620,17 @@
} }
}, },
"node_modules/@nestjs/swagger": { "node_modules/@nestjs/swagger": {
"version": "11.2.1", "version": "11.2.3",
"resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.1.tgz", "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.3.tgz",
"integrity": "sha512-1MS7xf0pzc1mofG53xrrtrurnziafPUHkqzRm4YUVPA/egeiMaSerQBD/feiAeQ2BnX0WiLsTX4HQFO0icvOjQ==", "integrity": "sha512-a0xFfjeqk69uHIUpP8u0ryn4cKuHdra2Ug96L858i0N200Hxho+n3j+TlQXyOF4EstLSGjTfxI1Xb2E1lUxeNg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@microsoft/tsdoc": "0.15.1", "@microsoft/tsdoc": "0.16.0",
"@nestjs/mapped-types": "2.1.0", "@nestjs/mapped-types": "2.1.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"path-to-regexp": "8.3.0", "path-to-regexp": "8.3.0",
"swagger-ui-dist": "5.29.4" "swagger-ui-dist": "5.30.2"
}, },
"peerDependencies": { "peerDependencies": {
"@fastify/static": "^8.0.0", "@fastify/static": "^8.0.0",
@@ -7814,9 +7814,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@@ -10202,9 +10202,9 @@
} }
}, },
"node_modules/swagger-ui-dist": { "node_modules/swagger-ui-dist": {
"version": "5.29.4", "version": "5.30.2",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.4.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.2.tgz",
"integrity": "sha512-gJFDz/gyLOCQtWwAgqs6Rk78z9ONnqTnlW11gimG9nLap8drKa3AJBKpzIQMIjl5PD2Ix+Tn+mc/tfoT2tgsng==", "integrity": "sha512-HWCg1DTNE/Nmapt+0m2EPXFwNKNeKK4PwMjkwveN/zn1cV2Kxi9SURd+m0SpdcSgWEK/O64sf8bzXdtUhigtHA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@scarf/scarf": "=1.4.0" "@scarf/scarf": "=1.4.0"

View File

@@ -30,7 +30,7 @@
"@nestjs/mapped-types": "^2.1.0", "@nestjs/mapped-types": "^2.1.0",
"@nestjs/passport": "^11.0.5", "@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.1", "@nestjs/swagger": "^11.2.3",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",

View File

@@ -9,6 +9,7 @@ import {
Query, Query,
ParseIntPipe, ParseIntPipe,
UseGuards, UseGuards,
ValidationPipe,
} from '@nestjs/common'; } from '@nestjs/common';
import { CalendarService } from './calendar.service'; import { CalendarService } from './calendar.service';
import { GetCalendarDto } from './dto/get-calendar.dto'; import { GetCalendarDto } from './dto/get-calendar.dto';
@@ -18,8 +19,9 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from '../auth/roles.guard'; import { RolesGuard } from '../auth/roles.guard';
import { Roles } from '../auth/roles.decorator'; import { Roles } from '../auth/roles.decorator';
import { Role } from '../auth/role.enum'; import { Role } from '../auth/role.enum';
import { CreateBookingDto } from './dto/create-booking.dto'; import { CalendarCreateBookingDto } from './dto/create-booking.dto';
import { CancelBookingDto } from './dto/cancel-booking.dto'; import { CancelBookingDto } from './dto/cancel-booking.dto';
import { ApiBody } from '@nestjs/swagger';
@Controller('calendar') @Controller('calendar')
@UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)
@@ -73,9 +75,10 @@ export class CalendarController {
// Create a booking for a specific event occurrence // Create a booking for a specific event occurrence
@Post('events/:id/bookings') @Post('events/:id/bookings')
@ApiBody({ type: CalendarCreateBookingDto })
createBooking( createBooking(
@Param('id', ParseIntPipe) eventId: number, @Param('id', ParseIntPipe) eventId: number,
@Body() createBookingDto: CreateBookingDto, @Body(new ValidationPipe()) createBookingDto: CalendarCreateBookingDto,
) { ) {
return this.calendarService.createBooking(eventId, createBookingDto); return this.calendarService.createBooking(eventId, createBookingDto);
} }

View File

@@ -14,7 +14,7 @@ import { CreateEventDto } from './dto/create-event.dto';
import { CreateExceptionDto } from './dto/create-exception.dto'; import { CreateExceptionDto } from './dto/create-exception.dto';
import { Booking } from '../entity/booking.entity'; import { Booking } from '../entity/booking.entity';
import { CancelBookingDto } from './dto/cancel-booking.dto'; import { CancelBookingDto } from './dto/cancel-booking.dto';
import { CreateBookingDto } from './dto/create-booking.dto'; import { CalendarCreateBookingDto } from './dto/create-booking.dto';
import { EventType } from '../entity/event-type.entity'; import { EventType } from '../entity/event-type.entity';
// --- Type-Safe Maps --- // --- Type-Safe Maps ---
@@ -520,7 +520,7 @@ export class CalendarService {
async createBooking( async createBooking(
eventId: number, eventId: number,
createBookingDto: CreateBookingDto, createBookingDto: CalendarCreateBookingDto,
): Promise<Booking> { ): Promise<Booking> {
const { occurrenceStartTime, userId } = createBookingDto; const { occurrenceStartTime, userId } = createBookingDto;

View File

@@ -7,23 +7,28 @@ import {
Min, Min,
} from 'class-validator'; } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
export class CreateBookingDto { export class CalendarCreateBookingDto {
@IsNotEmpty() @IsNotEmpty()
@Type(() => Date) @Type(() => Date)
@IsDate() @IsDate()
@ApiProperty()
occurrenceStartTime: Date; occurrenceStartTime: Date;
@IsNotEmpty() @IsNotEmpty()
@IsInt() @IsInt()
@ApiProperty()
userId: number; // Replaces userName/userEmail userId: number; // Replaces userName/userEmail
@IsOptional() @IsOptional()
@IsInt() @IsInt()
@Min(1) @Min(1)
@ApiProperty()
reservedSeatsCount?: number = 1; reservedSeatsCount?: number = 1;
@IsOptional() @IsOptional()
@IsString() @IsString()
@ApiProperty()
notes?: string; notes?: string;
} }