basic single event dashboard

This commit is contained in:
Schneider Roland 2025-11-28 08:03:33 +01:00
parent 02442a162a
commit aec1fd5ad1
11 changed files with 202 additions and 48 deletions

View File

@ -34,7 +34,13 @@ export class DetailView<T> {
config = input.required<DetailViewConfig<T>>(); config = input.required<DetailViewConfig<T>>();
getFormattedValue(row: DetailViewRow<T>, data: T): string { getFormattedValue(row: DetailViewRow<T>, data: T): string {
const value = row.getValue ? row.getValue(data) : data[row.attribute]; let value : any;
try {
value = row.getValue ? row.getValue(data) : data[row.attribute];
}catch (e) {
value = '';
}
if (row.component) { if (row.component) {
return ''; return '';

View File

@ -6,7 +6,7 @@
</div> </div>
<full-calendar #calendar [options]="calendarOptions"></full-calendar> <full-calendar #calendar [options]="calendarOptions"></full-calendar>
</div> </div>
{{"workflow:"+workflow()}}
<rs-daisy-modal [isOpen]="workflow() == 'create'" (closeClick)="closeDialog()"> <rs-daisy-modal [isOpen]="workflow() == 'create'" (closeClick)="closeDialog()">
@if (workflow() == 'create') { @if (workflow() == 'create') {
<app-create-event-form (ready)="closeDialog()" [date]="selectedDate()" <app-create-event-form (ready)="closeDialog()" [date]="selectedDate()"
@ -14,10 +14,29 @@
} }
</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> <app-single-event-dashboard [event]="selectedEvent()"
(action)="onDashboardAction($event)"
></app-single-event-dashboard>
</rs-daisy-modal> </rs-daisy-modal>
} }
<!--@if (workflow() == 'event-delete' && selectedEvent()) {
<rs-daisy-modal [isOpen]="true" (closeClick)="closeDialog()" []="'max-w-none w-2xl'">
delete event
</rs-daisy-modal>
}-->
@if (workflow() == 'event-cancel' && selectedEvent()) {
<rs-daisy-modal [isOpen]="true" (closeClick)="closeDialog()" [modalBoxStyleClass]="'max-w-none w-2xl'">
cancel
</rs-daisy-modal>
}
@for (dialogDefinition of dialogs; track dialogDefinition) {
@if (dialogDefinition.isRendered()) {
<rs-daisy-modal [isOpen]="true" (closeClick)="closeDialog()" [modalBoxStyleClass]="'max-w-none w-2xl'">
<ng-container *ngComponentOutlet="dialogDefinition.component; inputs: dialogDefinition.componentInputs ? dialogDefinition.componentInputs() : {}"></ng-container>
</rs-daisy-modal>
}
}

View File

@ -1,4 +1,4 @@
import { AfterViewInit, Component, effect, ElementRef, inject, OnInit, signal, ViewChild } from '@angular/core'; import { AfterViewInit, Component, effect, ElementRef, inject, OnInit, signal, Type, ViewChild } from '@angular/core';
import { FullCalendarComponent, FullCalendarModule } from '@fullcalendar/angular'; import { FullCalendarComponent, FullCalendarModule } from '@fullcalendar/angular';
import { CalendarOptions, EventInput } from '@fullcalendar/core'; import { CalendarOptions, EventInput } from '@fullcalendar/core';
@ -6,17 +6,20 @@ import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid'; import timeGridPlugin from '@fullcalendar/timegrid';
import listPlugin from '@fullcalendar/list'; import listPlugin from '@fullcalendar/list';
import interactionPlugin from '@fullcalendar/interaction'; import interactionPlugin from '@fullcalendar/interaction';
import { Modal } from '@rschneider/ng-daisyui'; import { Button, Modal } from '@rschneider/ng-daisyui';
import { CreateEventForm } from '../create-event-form/create-event-form'; import { CreateEventForm } from '../create-event-form/create-event-form';
import { CalendarService } from '../../services/calendar.service'; import { CalendarService } from '../../services/calendar.service';
import { CalendarEventDto, EventsInRangeDTO } from '../../models/events-in-range-dto.model'; import { CalendarEventDto, EventsInRangeDTO } from '../../models/events-in-range-dto.model';
import { map } from 'rxjs'; import { map } from 'rxjs';
import { SingleEventDashboard } from './single-event-dashboard/single-event-dashboard'; import { SingleEventDashboard } from './single-event-dashboard/single-event-dashboard';
import { JsonPipe } from '@angular/common'; import { JsonPipe, NgComponentOutlet } from '@angular/common';
import {
SingleEventDashboardEventDelete
} from './single-event-dashboard-event-delete/single-event-dashboard-event-delete';
@Component({ @Component({
selector: 'app-calendar-view', selector: 'app-calendar-view',
imports: [FullCalendarModule, Modal, CreateEventForm, SingleEventDashboard, JsonPipe], imports: [FullCalendarModule, Modal,NgComponentOutlet, CreateEventForm, SingleEventDashboard, JsonPipe],
templateUrl: './calendar-view.html', templateUrl: './calendar-view.html',
styleUrl: './calendar-view.css', styleUrl: './calendar-view.css',
}) })
@ -32,6 +35,7 @@ export class CalendarView implements OnInit, AfterViewInit {
selectedEvent = signal<CalendarEventDto|undefined>(undefined) selectedEvent = signal<CalendarEventDto|undefined>(undefined)
calendarOptions: CalendarOptions; calendarOptions: CalendarOptions;
dialogs: DialogConfig[] = [];
constructor() { constructor() {
@ -62,6 +66,21 @@ export class CalendarView implements OnInit, AfterViewInit {
}; };
this.dialogs = [
{
component: SingleEventDashboardEventDelete,
isRendered: () => this.workflow() == 'event-delete',
// isRendered: () => true,
closeClick: () => this.closeDialog(),
modalBoxStyleClass: 'max-w-none w-2xl',
componentInputs: () => {
return {
}
}
}
]
} }
fetchEvents(fetchInfo: any, successCallback: (events: EventInput[]) => void, failureCallback: (error: any) => void): void { fetchEvents(fetchInfo: any, successCallback: (events: EventInput[]) => void, failureCallback: (error: any) => void): void {
@ -139,4 +158,23 @@ export class CalendarView implements OnInit, AfterViewInit {
closeDialog() { closeDialog() {
this.workflow.set(''); this.workflow.set('');
} }
onDashboardAction(action: string){
console.info("dashboard event", action);
switch (action) {
case 'event_delete':
this.workflow.set('event-delete');
break;
}
}
}
export interface DialogConfig{
component: Type<any>;
componentInputs?: () => { [key: string]: any };
closeClick: () => void,
modalBoxStyleClass: string
isRendered: () => boolean;
} }

View File

@ -3,7 +3,7 @@
<h2 class="card-title text-center">{{ title() }}</h2> <h2 class="card-title text-center">{{ title() }}</h2>
<p class="text-center">{{ description() }}</p> <p class="text-center">{{ description() }}</p>
<div class="card-actions justify-end"> <div class="card-actions justify-end">
<button class="btn btn-primary"> <button class="btn btn-primary" (click)="onClick();">
<!-- Using the new @if control flow --> <!-- Using the new @if control flow -->
@if (svgIcon()) { @if (svgIcon()) {
<span [innerHTML]="svgIcon() | safeHtml"></span> <span [innerHTML]="svgIcon() | safeHtml"></span>

View File

@ -1,4 +1,4 @@
import { Component, computed, HostBinding, input } from '@angular/core'; import { Component, computed, HostBinding, input, output } from '@angular/core';
import { SafeHtmlPipe } from '../../../../../pipes/safe-html-pipe'; import { SafeHtmlPipe } from '../../../../../pipes/safe-html-pipe';
@Component({ @Component({
@ -11,10 +11,12 @@ import { SafeHtmlPipe } from '../../../../../pipes/safe-html-pipe';
styleUrl: './single-event-dashboard-card.css', styleUrl: './single-event-dashboard-card.css',
}) })
export class SingleEventDashboardCard { export class SingleEventDashboardCard {
title= input<string>(); title = input<string>();
description= input<string>(); description = input<string>();
buttonTitle= input<string>(); buttonTitle = input<string>();
svgIcon= input<string>(); svgIcon = input<string>();
onAction = output<void>();
// 1. Define the Input Signal to receive class names as a string or array // 1. Define the Input Signal to receive class names as a string or array
// Example: 'grow-2 align-center' // Example: 'grow-2 align-center'
@ -41,4 +43,7 @@ export class SingleEventDashboardCard {
return this.hostClasses(); return this.hostClasses();
} }
protected onClick() {
this.onAction.emit();
}
} }

View File

@ -0,0 +1,5 @@
@if (config) {
<app-detail-view
[config]="config"
></app-detail-view>
}

View File

@ -0,0 +1,50 @@
import { Component, effect, input } from '@angular/core';
import { CalendarEventDto } from '../../../models/events-in-range-dto.model';
import { DetailView, DetailViewConfig } from '../../../../../components/detail-view/detail-view';
@Component({
selector: 'app-single-event-dashboard-event-delete',
imports: [
DetailView,
],
templateUrl: './single-event-dashboard-event-delete.html',
styleUrl: './single-event-dashboard-event-delete.css',
})
export class SingleEventDashboardEventDelete {
event = input<CalendarEventDto>();
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,
},
],
};
});
}
}

View File

@ -15,6 +15,7 @@
[description]="card.description" [description]="card.description"
[buttonTitle]="card.buttonTitle" [buttonTitle]="card.buttonTitle"
[styleClass]="'flex-1'" [styleClass]="'flex-1'"
(onAction)="onCardAction(card.action)"
></app-single-event-dashboard-card> ></app-single-event-dashboard-card>
<!-- [customClasses]="'flex-[50_1_0] w-50' "--> <!-- [customClasses]="'flex-[50_1_0] w-50' "-->

View File

@ -1,4 +1,4 @@
import { Component, effect, input } from '@angular/core'; import { Component, effect, input, output } 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 { DetailView, DetailViewConfig } from '../../../../../components/detail-view/detail-view';
import { SvgIcons } from '../../../../../svg-icons'; import { SvgIcons } from '../../../../../svg-icons';
@ -16,42 +16,15 @@ import { SingleEventDashboardCard } from '../single-event-dashboard-card/single-
export class SingleEventDashboard { export class SingleEventDashboard {
event = input<CalendarEventDto>(); event = input<CalendarEventDto>();
action = output<string>();
config: DetailViewConfig<CalendarEventDto> | undefined; config: DetailViewConfig<CalendarEventDto> | undefined;
cards: CardConfig[] = [ cards: CardConfig[] = [];
{
buttonTitle: 'Szerkesztés',
title: 'Szerkesztés',
svgIcon: SvgIcons.heorPencilSquare,
description: 'Az esemény módosítása',
},
{
buttonTitle: 'Lemondás',
title: 'Esemény lemondása',
svgIcon: SvgIcons.heroXcircle,
description: 'Az esemény lemondása, email küldése a jelentkezőknek',
},
{
buttonTitle: 'Törlése',
title: 'Esemény törlése',
svgIcon: SvgIcons.heroTrash,
description: 'Az esemény törlése',
},
{
buttonTitle: 'Bejelentkezés',
title: 'Időpont foglalás',
svgIcon: SvgIcons.heroUserPlus,
description: 'Időpont foglalása eseményre',
},
{
buttonTitle: 'Lemondás',
title: 'Lemondása',
svgIcon: SvgIcons.heroUserMinus,
description: 'Az időpont lemondása',
},
];
constructor() { constructor() {
effect(() => { effect(() => {
this.config = { this.config = {
data: this.event()!, data: this.event()!,
@ -78,8 +51,60 @@ export class SingleEventDashboard {
], ],
}; };
}); });
this.cards = [
{
buttonTitle: 'Szerkesztés',
title: 'Szerkesztés',
svgIcon: SvgIcons.heorPencilSquare,
description: 'Az esemény módosítása',
action: 'event_edit',
},
{
buttonTitle: 'Lemondás',
title: 'Esemény lemondása',
svgIcon: SvgIcons.heroXcircle,
description: 'Az esemény lemondása',
action: 'event_cancel',
},
{
buttonTitle: 'Törlés',
title: 'Esemény törlése',
svgIcon: SvgIcons.heroTrash,
description: 'Az esemény törlése',
action: 'event_delete',
},
{
buttonTitle: 'Megnézem',
title: 'Foglalások',
svgIcon: SvgIcons.heroUserGroup,
description: 'Foglalások megtekintése',
action: 'event_bookings',
},
{
buttonTitle: 'Bejelentkezés',
title: 'Időpont foglalás',
svgIcon: SvgIcons.heroUserPlus,
description: 'Időpont foglalása eseményre',
action: 'user_booking',
},
{
buttonTitle: 'Lemondás',
title: 'Lemondás',
svgIcon: SvgIcons.heroUserMinus,
description: 'Az időpont lemondása',
action: 'user_cancel',
},
];
} }
onCardAction (action: string|undefined) {
if ( action ){
this.action.emit(action);
}
console.info("card action", action);
}
protected readonly SvgIcons = SvgIcons; protected readonly SvgIcons = SvgIcons;
} }
@ -88,4 +113,5 @@ export interface CardConfig {
description?: string; description?: string;
buttonTitle?: string; buttonTitle?: string;
svgIcon?: string; svgIcon?: string;
action?: string;
} }

View File

@ -35,5 +35,9 @@ export class SvgIcons {
` `
public static heroUserGroup = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />
</svg>
`
} }