add calendar dashboard edit

This commit is contained in:
Schneider Roland
2025-12-08 16:40:19 +01:00
parent cacc04a217
commit 2e2f37ab86
16 changed files with 194 additions and 69 deletions

View File

@@ -24,8 +24,8 @@ import {
SingleEventDashboardEventDelete, SingleEventDashboardEventDelete,
} from './single-event-dashboard-event-delete/single-event-dashboard-event-delete'; } from './single-event-dashboard-event-delete/single-event-dashboard-event-delete';
import { import {
SingleEventDashboardEventCancel, SingleEventDashboardEventActivation,
} from './single-event-dashboard-event-cancel/single-event-dashboard-event-cancel'; } 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';
@Component({ @Component({
@@ -81,7 +81,6 @@ export class CalendarView {
{ {
component: SingleEventDashboardEventDelete, component: SingleEventDashboardEventDelete,
isRendered: () => this.workflow() == 'event-delete', isRendered: () => this.workflow() == 'event-delete',
// isRendered: () => true,
closeClick: () => this.closeDialog(), closeClick: () => this.closeDialog(),
modalBoxStyleClass: 'max-w-none w-2xl', modalBoxStyleClass: 'max-w-none w-2xl',
componentInputs: () => { componentInputs: () => {
@@ -94,13 +93,26 @@ export class CalendarView {
}, },
{ {
component: SingleEventDashboardEventCancel, component: SingleEventDashboardEventActivation,
isRendered: () => this.workflow() == 'event-cancel', isRendered: () => this.workflow() == 'event-cancel',
// isRendered: () => true,
closeClick: () => this.closeDialog(), closeClick: () => this.closeDialog(),
modalBoxStyleClass: 'max-w-none w-2xl', modalBoxStyleClass: 'max-w-none w-2xl',
componentInputs: () => { componentInputs: () => {
return { return {
'mode': 'cancel',
'event': this.selectedEvent(),
'onAction': this.handleAction,
};
},
},
{
component: SingleEventDashboardEventActivation,
isRendered: () => this.workflow() == 'event_activate',
closeClick: () => this.closeDialog(),
modalBoxStyleClass: 'max-w-none w-2xl',
componentInputs: () => {
return {
'mode': 'activate',
'event': this.selectedEvent(), 'event': this.selectedEvent(),
'onAction': this.handleAction, 'onAction': this.handleAction,
}; };
@@ -141,6 +153,12 @@ export class CalendarView {
extendedProps: { extendedProps: {
event: model, event: model,
}, },
editable: !model.isCancelled,
// 2. Add a class for styling
classNames: model.isCancelled ? ['disabled-event'] : [],
// Optional: Force a gray color here if not using CSS
backgroundColor: '#d3d3d3',
borderColor: '#d3d3d3'
}; };
if (model.eventType) { if (model.eventType) {
calendarEvent.borderColor = model.eventType.color; calendarEvent.borderColor = model.eventType.color;
@@ -197,6 +215,9 @@ export class CalendarView {
case 'event_edit': case 'event_edit':
this.workflow.set('event-edit'); this.workflow.set('event-edit');
break; break;
default:
this.workflow.set(action);
break;
} }
} }
@@ -209,6 +230,7 @@ export class CalendarView {
} else if ( msg == 'save-event-success'){ } else if ( msg == 'save-event-success'){
this.closeDialog(); this.closeDialog();
this.calendarComponent?.getApi().refetchEvents(); this.calendarComponent?.getApi().refetchEvents();
}else{
} }
}; };

View File

@@ -0,0 +1,27 @@
<h2 class="text-2xl">
@if (mode() == 'cancel') {
Esemény lemondása
} @else {
Esemény aktiválása
}
</h2>
<app-single-event-dashboard-event-details-view [event]="event()"></app-single-event-dashboard-event-details-view>
<div class="flex gap-2 mt-3">
@if (mode() == 'cancel') {
<rs-daisy-button variant="error" (click)="cancelEventOccurrence()">
<span [outerHTML]="SvgIcons.heroTrash | safeHtml"></span>
Lemondás
</rs-daisy-button>
}
@if (mode() == 'activate') {
<rs-daisy-button variant="error" (click)="activateEventOccurrence()">
<span [outerHTML]="SvgIcons.heroTrash | safeHtml"></span>
Aktiválás
</rs-daisy-button>
}
<rs-daisy-button variant="primary" (click)="closeDialog()">
<span [outerHTML]="SvgIcons.heroXcircle | safeHtml"></span>
Mégsem
</rs-daisy-button>
</div>

View File

@@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SingleEventDashboardEventCancel } from './single-event-dashboard-event-cancel'; import { SingleEventDashboardEventActivation } from './single-event-dashboard-event-activation.component';
describe('SingleEventDashboardEventCancel', () => { describe('SingleEventDashboardEventCancel', () => {
let component: SingleEventDashboardEventCancel; let component: SingleEventDashboardEventActivation;
let fixture: ComponentFixture<SingleEventDashboardEventCancel>; let fixture: ComponentFixture<SingleEventDashboardEventActivation>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [SingleEventDashboardEventCancel] imports: [SingleEventDashboardEventActivation]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(SingleEventDashboardEventCancel); fixture = TestBed.createComponent(SingleEventDashboardEventActivation);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -0,0 +1,63 @@
import { Component, inject, input } from '@angular/core';
import { CalendarEventDto } from '../../../models/events-in-range-dto.model';
import {
SingleEventDashboardEventDetailsView
} from '../single-event-dashboard-event-details-view/single-event-dashboard-event-details-view';
import { Button } from '@rschneider/ng-daisyui';
import { SvgIcons } from '../../../../../svg-icons';
import { SafeHtmlPipe } from '../../../../../pipes/safe-html-pipe';
import { CalendarService } from '../../../services/calendar.service';
export type ACTIVATION_TYPE = 'cancel' | 'activate';
@Component({
selector: 'app-single-event-dashboard-event-cancel',
imports: [
SingleEventDashboardEventDetailsView,
Button,
SafeHtmlPipe,
],
templateUrl: './single-event-dashboard-event-activation.component.html',
styleUrl: './single-event-dashboard-event-activation.component.css',
})
export class SingleEventDashboardEventActivation {
mode = input<ACTIVATION_TYPE>('cancel');
calendarService = inject(CalendarService);
event = input<CalendarEventDto>();
onAction = input.required<(msg: string) => void>();
protected readonly SvgIcons = SvgIcons;
protected setEventOccurrenceActivation(activated: boolean) {
const eventId =this.event()?.id!;
const startTime = this.event()?.startTime!;
this.calendarService.applyException(eventId,{
originalStartTime: new Date(startTime),
isCancelled: !activated,
}).subscribe(
{
next: () => {
this.onAction()('close');
},
error: err => {
alert("Failed to change event");
}
}
)
}
protected cancelEventOccurrence() {
this.setEventOccurrenceActivation(false);
}
protected activateEventOccurrence() {
this.setEventOccurrenceActivation(true);
}
protected closeDialog() {
this.onAction()('close')
}
}

View File

@@ -1,12 +0,0 @@
<h2 class="text-2xl">Esemény lemondása</h2>
<app-single-event-dashboard-event-details-view [event]="event()"></app-single-event-dashboard-event-details-view>
<div class="flex gap-2 mt-3">
<rs-daisy-button variant="error">
<span [outerHTML]="SvgIcons.heroTrash | safeHtml"></span>
Törlés
</rs-daisy-button>
<rs-daisy-button variant="primary" (click)="triggerAction()">
<span [outerHTML]="SvgIcons.heroXcircle | safeHtml"></span>
Mégsem
</rs-daisy-button>
</div>

View File

@@ -1,31 +0,0 @@
import { Component, input } from '@angular/core';
import { CalendarEventDto } from '../../../models/events-in-range-dto.model';
import {
SingleEventDashboardEventDetailsView
} from '../single-event-dashboard-event-details-view/single-event-dashboard-event-details-view';
import { Button } from '@rschneider/ng-daisyui';
import { SvgIcons } from '../../../../../svg-icons';
import { SafeHtmlPipe } from '../../../../../pipes/safe-html-pipe';
@Component({
selector: 'app-single-event-dashboard-event-cancel',
imports: [
SingleEventDashboardEventDetailsView,
Button,
SafeHtmlPipe,
],
templateUrl: './single-event-dashboard-event-cancel.html',
styleUrl: './single-event-dashboard-event-cancel.css',
})
export class SingleEventDashboardEventCancel {
event = input<CalendarEventDto>();
onAction = input.required<(msg: string) => void>();
protected readonly SvgIcons = SvgIcons;
protected triggerAction() {
this.onAction()('close')
}
}

View File

@@ -1,5 +1,9 @@
<h2 class="text-xl">Esemény</h2> @if (event()?.isRecurring) {
<h2 class="text-xl">Esemény sorozat</h2>
}
@if (!event()?.isRecurring) {
<h2 class="text-xl">Esemény </h2>
}
@if (config) { @if (config) {
<app-detail-view <app-detail-view
[config]="config" [config]="config"
@@ -7,7 +11,7 @@
} }
<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) {
<app-single-event-dashboard-card <app-single-event-dashboard-card
[title]="card.title" [title]="card.title"

View File

@@ -1,4 +1,4 @@
import { Component, effect, input, output } from '@angular/core'; import { Component, effect, input, output, signal } 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';
@@ -19,13 +19,14 @@ export class SingleEventDashboard {
action = output<string>(); action = output<string>();
config: DetailViewConfig<CalendarEventDto> | undefined; config: DetailViewConfig<CalendarEventDto> | undefined;
cards: CardConfig[] = []; cards = signal<CardConfig[]>([]);
constructor() { constructor() {
effect(() => { effect(() => {
console.info("dashboard", this.event());
this.config = { this.config = {
data: this.event()!, data: this.event()!,
@@ -50,8 +51,8 @@ export class SingleEventDashboard {
}, },
], ],
}; };
});
this.cards = [ this.cards.set( [
{ {
buttonTitle: 'Szerkesztés', buttonTitle: 'Szerkesztés',
title: 'Szerkesztés', title: 'Szerkesztés',
@@ -59,11 +60,21 @@ export class SingleEventDashboard {
description: 'Az esemény módosítása', description: 'Az esemény módosítása',
action: 'event_edit', action: 'event_edit',
}, },
this.event()?.isCancelled ?
{
buttonTitle: 'Aktiválás',
title: 'Előfordulás aktiválása',
svgIcon: SvgIcons.heroPlay,
description: 'Az esemény ezen előfordulásának aktiválása',
action: 'event_activate',
} :
{ {
buttonTitle: 'Lemondás', buttonTitle: 'Lemondás',
title: 'Esemény lemondása', title: 'Előfordulás lemondása',
svgIcon: SvgIcons.heroXcircle, svgIcon: SvgIcons.heroXcircle,
description: 'Az esemény lemondása', description: 'Az esemény ezen előfordulásának lemondása',
action: 'event_cancel', action: 'event_cancel',
}, },
{ {
@@ -94,7 +105,8 @@ export class SingleEventDashboard {
description: 'Az időpont lemondása', description: 'Az időpont lemondása',
action: 'user_cancel', action: 'user_cancel',
}, },
]; ]);
});
} }
onCardAction (action: string|undefined) { onCardAction (action: string|undefined) {

View File

@@ -0,0 +1,8 @@
export interface CreateExceptionDto {
originalStartTime: Date; // The start time of the instance to modify/cancel
isCancelled?: boolean;
newStartTime?: Date;
newEndTime?: Date;
title?: string;
description?: string;
}

View File

@@ -25,6 +25,8 @@ export type CalendarEventDto = {
description: string, description: string,
isModified?: boolean; isModified?: boolean;
eventBookings: BookingWithUserDto[]; eventBookings: BookingWithUserDto[];
isCancelled: boolean;
isRecurring: boolean;
eventType: EventType eventType: EventType
}; };

View File

@@ -5,6 +5,7 @@ import { ConfigurationService } from '../../../services/configuration.service';
import { EventFormDTO, UpdateEventFormDTO } from '../models/event-form-dto.model'; import { EventFormDTO, UpdateEventFormDTO } from '../models/event-form-dto.model';
import { Event } from '../../events/models/event.model'; import { Event } from '../../events/models/event.model';
import { CalendarEventDto, EventsInRangeDTO } from '../models/events-in-range-dto.model'; import { CalendarEventDto, EventsInRangeDTO } from '../models/events-in-range-dto.model';
import { CreateExceptionDto } from '../models/event-exception.model';
@Injectable({ @Injectable({
@@ -40,4 +41,7 @@ export class CalendarService {
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){
return this.http.post(this.apiUrl+`/events/${eventId}/exceptions`, eventException);
}
} }

View File

@@ -39,5 +39,9 @@ export class SvgIcons {
<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" /> <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> </svg>
` `
public static heroPlay = `<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="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z" />
</svg>
`;
} }

View File

@@ -4,3 +4,14 @@
@import "./styles/grid.css"; @import "./styles/grid.css";
@source "../projects/rschneider/ng-daisyui/src"; @source "../projects/rschneider/ng-daisyui/src";
@plugin "daisyui"; @plugin "daisyui";
.disabled-event {
/* Make it look faded */
opacity: 0.5;
/* Make it un-clickable (stops hover effects and click events) */
/*pointer-events: none;*/
/* Optional: Change cursor to indicate it's not interactive */
/*cursor: not-allowed;*/
}

View File

@@ -51,6 +51,7 @@ type BookingWithUserDto = {
// The final shape of a calendar event occurrence // The final shape of a calendar event occurrence
export type CalendarEventDto = Omit<Event, 'bookings'> & { export type CalendarEventDto = Omit<Event, 'bookings'> & {
isModified?: boolean; isModified?: boolean;
isCancelled?: boolean;
eventBookings: BookingWithUserDto[]; eventBookings: BookingWithUserDto[];
}; };
@@ -170,16 +171,20 @@ export class CalendarService {
); );
if (exception) { if (exception) {
if (exception.isCancelled) continue; // if (exception.isCancelled) continue;
// This is a MODIFIED occurrence // This is a MODIFIED occurrence
const key = `${event.id}-${exception.newStartTime.getTime()}`; const key = `${event.id}-${exception.newStartTime?.getTime() || occurrenceDate.getTime()}`;
recurringOccurrences.push({ recurringOccurrences.push({
...event, ...event,
startTime: exception.newStartTime, // startTime: exception.newStartTime || occurrenceDate,
endTime: exception.newEndTime, startTime: exception.newStartTime || occurrenceDate,
endTime:
exception.newEndTime ||
new Date(occurrenceDate.getTime() + duration),
isModified: true, isModified: true,
eventBookings: bookingMap.get(key) || [], eventBookings: bookingMap.get(key) || [],
isCancelled: !!exception.isCancelled,
}); });
} else { } else {
// This is a REGULAR occurrence // This is a REGULAR occurrence

View File

@@ -1,4 +1,10 @@
import { IsDate, IsNotEmpty, IsOptional, IsString, IsBoolean } from 'class-validator'; import {
IsDate,
IsNotEmpty,
IsOptional,
IsString,
IsBoolean,
} from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
export class CreateExceptionDto { export class CreateExceptionDto {
@@ -28,4 +34,4 @@ export class CreateExceptionDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
description?: string; description?: string;
} }