feat: Implement event dashboard with activation, cancellation, editing, and booking management functionalities.

This commit is contained in:
Schneider Roland
2025-12-31 12:00:05 +01:00
parent 90c192c881
commit 7ed3367ed8
21 changed files with 441 additions and 274 deletions

66
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,66 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${file}"
},
{
"name": "NPM Start Admin",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/admin",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"start"
],
"console": "integratedTerminal",
},
{
"name": "NPM Start LIB",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/admin",
"runtimeExecutable": "ng",
"runtimeArgs": [
"build",
"@rschneider/ng-daisyui",
"--watch"
],
"console": "integratedTerminal"
},
{
"name": "NPM Start REST",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/server",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"start:dev"
],
"preLaunchTask": "Docker Up (Environment)",
"console": "integratedTerminal"
}
],
"compounds": [
{
"name": "Full Stack: Docker + App + GUI",
"configurations": [
"NPM Start LIB",
"NPM Start Admin",
"NPM Start REST"
],
"stopAll": true
}
]
}

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"editor.fontFamily": "'FiraCode Nerd Font Mono',Menlo, Monaco, 'Courier New', monospace",
"terminal.integrated.fontFamily": "'FiraCode Nerd Font Mono'",
"markdown.preview.fontFamily": "'FiraCode Nerd Font Mono',-apple-system, BlinkMacSystemFont, 'Segoe WPC', 'Segoe UI', system-ui, 'Ubuntu', 'Droid Sans', sans-serif"
}

18
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,18 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Docker Up (Environment)",
"type": "shell",
"command": "docker compose up -d",
"options": {
"cwd": "${workspaceFolder}/environment/dev"
},
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -78,6 +78,7 @@ export interface CalendarCreateBookingDto {
} }
export interface CancelBookingDto { export interface CancelBookingDto {
canceledReason: string;
} }
export interface UserResponseDto { export interface UserResponseDto {
@@ -94,6 +95,7 @@ export interface BookingResponseDto {
canceledAt: string | null; canceledAt: string | null;
createdAt: string | null; createdAt: string | null;
user: UserResponseDto; user: UserResponseDto;
status?: string;
} }
export interface PaginationResponseMetaDto { export interface PaginationResponseMetaDto {

View File

@@ -137,9 +137,9 @@ export class CalendarService {
return this.httpClient.post(url, calendarCreateBookingDto, requestOptions); return this.httpClient.post(url, calendarCreateBookingDto, requestOptions);
} }
calendarControllerCancelBooking(bookingId: number, cancelBookingDto: CancelBookingDto, observe?: 'body', options?: RequestOptions<'json'>): Observable<any>; calendarControllerCancelBooking(bookingId: number, cancelBookingDto: CancelBookingDto, observe?: 'body', options?: RequestOptions<'json'>): Observable<CalenderControllerGetBookingResponse>;
calendarControllerCancelBooking(bookingId: number, cancelBookingDto: CancelBookingDto, observe?: 'response', options?: RequestOptions<'json'>): Observable<HttpResponse<any>>; calendarControllerCancelBooking(bookingId: number, cancelBookingDto: CancelBookingDto, observe?: 'response', options?: RequestOptions<'json'>): Observable<HttpResponse<CalenderControllerGetBookingResponse>>;
calendarControllerCancelBooking(bookingId: number, cancelBookingDto: CancelBookingDto, observe?: 'events', options?: RequestOptions<'json'>): Observable<HttpEvent<any>>; calendarControllerCancelBooking(bookingId: number, cancelBookingDto: CancelBookingDto, observe?: 'events', options?: RequestOptions<'json'>): Observable<HttpEvent<CalenderControllerGetBookingResponse>>;
calendarControllerCancelBooking(bookingId: number, cancelBookingDto: CancelBookingDto, observe?: 'body' | 'events' | 'response', options?: RequestOptions<'arraybuffer' | 'blob' | 'json' | 'text'>): Observable<any> { calendarControllerCancelBooking(bookingId: number, cancelBookingDto: CancelBookingDto, observe?: 'body' | 'events' | 'response', options?: RequestOptions<'arraybuffer' | 'blob' | 'json' | 'text'>): Observable<any> {
const url = `${this.basePath}/api/calendar/bookings/${bookingId}/cancel`; const url = `${this.basePath}/api/calendar/bookings/${bookingId}/cancel`;

View File

@@ -5,26 +5,22 @@
@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)="setWorkFlow($event)"></app-single-event-dashboard>
(action)="setWorkFlow($event)" </rs-daisy-modal>
></app-single-event-dashboard>
</rs-daisy-modal>
} }
@for (dialogDefinition of dialogs; track dialogDefinition) { @for (dialogDefinition of dialogs; track dialogDefinition) {
@if (dialogDefinition.isRendered()) { @if (dialogDefinition.isRendered()) {
<rs-daisy-modal [isOpen]="true" (closeClick)="closeDialog()" [modalBoxStyleClass]="'max-w-none w-2xl'"> <rs-daisy-modal [isOpen]="true" (closeClick)="openDashboard()" [modalBoxStyleClass]="'max-w-none w-2xl'">
<ng-container <ng-container
*ngComponentOutlet="dialogDefinition.component; inputs: dialogDefinition.componentInputs ? dialogDefinition.componentInputs() : {}; " *ngComponentOutlet="dialogDefinition.component; inputs: dialogDefinition.componentInputs ? dialogDefinition.componentInputs() : {}; "></ng-container>
></ng-container> </rs-daisy-modal>
</rs-daisy-modal>
}
} }
}

View File

@@ -79,14 +79,24 @@ export class CalendarView {
} }
}); });
this.eventBus.on(EventType.CALENDAR_VIEW_DIALOG_CLOSED) this.eventBus.on(EventType.CALENDAR_VIEW_CLOSE_DIALOG_AND_RELOAD)
.pipe(takeUntilDestroyed()) .pipe(takeUntilDestroyed())
.subscribe({ .subscribe({
next: (_) => { next: (_) => {
this.closeDialog(); this.closeDialog();
this.calendarComponent?.getApi().refetchEvents();
} }
}) })
this.eventBus.on(EventType.CALENDAR_VIEW_EVENT_DASHBOARD)
.pipe(takeUntilDestroyed())
.subscribe({
next: (_) => {
this.openDashboard();
}
})
this.calendarOptions = { this.calendarOptions = {
plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin], plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin],
@@ -237,16 +247,16 @@ export class CalendarView {
}), }),
) )
.subscribe({ .subscribe({
next: (events) => { next: (events) => {
console.info('calendar events', events); console.info('calendar events', events);
successCallback(events); successCallback(events);
} }
, ,
error: (error) => { error: (error) => {
console.error('Error fetching events', error); console.error('Error fetching events', error);
failureCallback(error); failureCallback(error);
},
}, },
},
); );
} }
@@ -254,7 +264,9 @@ export class CalendarView {
closeDialog() { closeDialog() {
this.workflow.set('NO_DIALOG'); this.workflow.set('NO_DIALOG');
} }
openDashboard() {
this.workflow.set('event_dashboard');
}
/** /**
* Set dashboard workflow * Set dashboard workflow
* @param workflowType * @param workflowType

View File

@@ -2,37 +2,52 @@
Foglalások Foglalások
</app-heading> </app-heading>
@if (bookings.isLoading()) { @if (bookings.isLoading()) {
<div>loading...</div> <div>loading...</div>
} @else { } @else {
@if (bookings.value()?.data?.length) { @if (bookings.value()?.data?.length) {
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table"> <table class="table">
<!-- head --> <!-- head -->
<thead> <thead>
<tr> <tr>
<th>Foglalás dátuma</th> <th>Foglalás dátuma</th>
<th>Foglalt helyek száma</th> <th>Foglalt helyek száma</th>
<th></th> <th>Státusz</th>
</tr> <th></th>
</thead> </tr>
<tbody> </thead>
@for (booking of bookings.value()?.data; track booking) { <tbody>
<tr> @for (booking of bookings.value()?.data; track booking) {
<td>{{formatDateTime(booking.createdAt)}}</td> <tr>
<td>{{booking.reservedSeatsCount}}</td> <td>{{formatDateTime(booking.createdAt)}}</td>
<td><rs-daisy-button [variant]="'error'"> <td>{{booking.reservedSeatsCount}}</td>
Lemondás <td>
</rs-daisy-button></td> @switch (booking.status){
</tr> @case ('active'){
<span [outerHTML]="SvgIcons.heroCheckCircle | safeHtml"></span>
} }
</tbody> @case ('customer_cancelled'){
</table> <span [outerHTML]="SvgIcons.heroMinusCircle | safeHtml"></span>
</div> }
@if(bookings.value()?.meta?.totalPages! > 1){ }
<rs-daisy-pagination [pageCount]="pageCount()" [activePage]="activePage()" </td>
(onPaginate)="paginate($event)"></rs-daisy-pagination> <td>
} @if (booking.status == 'active') {
} @else { <rs-daisy-button [variant]="'error'" (clickEvent)="cancelBooking(booking)">
Nem találtunk foglalást erre az eseményre Lemondás
} </rs-daisy-button>
}
</td>
</tr>
}
</tbody>
</table>
</div>
@if(bookings.value()?.meta?.totalPages! > 1){
<rs-daisy-pagination [pageCount]="pageCount()" [activePage]="activePage()"
(onPaginate)="paginate($event)"></rs-daisy-pagination>
} }
} @else {
Nem találtunk foglalást erre az eseményre
}
}

View File

@@ -7,6 +7,9 @@ import { delay, of, throwError } from 'rxjs';
import { Button, Pagination } from '@rschneider/ng-daisyui'; import { Button, Pagination } from '@rschneider/ng-daisyui';
import { Heading } from '../../../../../components/heading/heading'; import { Heading } from '../../../../../components/heading/heading';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { EventType } from '../../../../../../types';
import { SvgIcons } from '../../../../../svg-icons';
import { SafeHtmlPipe } from "../../../../../pipes/safe-html-pipe";
@Component({ @Component({
selector: 'app-single-event-booking-list', selector: 'app-single-event-booking-list',
@@ -14,6 +17,7 @@ import { format } from 'date-fns';
Pagination, Pagination,
Heading, Heading,
Button, Button,
SafeHtmlPipe
], ],
templateUrl: './single-event-booking-list.html', templateUrl: './single-event-booking-list.html',
styleUrl: './single-event-booking-list.css', styleUrl: './single-event-booking-list.css',
@@ -30,57 +34,64 @@ export class SingleEventBookingList {
pageCount = computed(() => { pageCount = computed(() => {
// return this.bookings.value()?.pageCount || 1; // return this.bookings.value()?.pageCount || 1;
return 1; return 1;
}) });
bookings = rxResource( bookings = rxResource(
{ {
params: () => ({ params: () => ({
page: this.activePage() page: this.activePage(),
}), }),
stream: ({params}) => { stream: ({ params }) => {
// console.info("loading resource", params); if (!this.event()) {
// return throwError(() => new Error('no event'));
// const allData = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t"]
//
// let pageCount = Math.floor( allData.length / this.pageSize());
// if ( (allData.length % this.pageSize()) > 0){
// pageCount += 1;
// }
// pageCount = Math.max(pageCount ,1);
//
// const pageData = allData.slice( ((this.activePage()-1) * this.pageSize()),this.activePage()*this.pageSize());
// console.info("booking page data",pageData);
// return of({
// items: pageData,
// pageCount
// }).pipe( delay(1000))
if ( !this.event()){
return throwError( () => new Error("no event"))
} }
const event = this.event()!; const event = this.event()!;
return this.calendarService.calendarControllerGetBookings( return this.calendarService.calendarControllerGetBookings(
event.id, event.id,
new Date(event.startTime).toISOString() new Date(event.startTime).toISOString(),
) params.page,
this.pageSize(),
);
}, },
} },
) );
protected paginate($event: number) { protected paginate($event: number) {
console.info("paginated to ", $event) console.info('paginated to ', $event);
this.activePage.set($event); this.activePage.set($event);
} }
formatDateTime( dateStr?: string|Date|null){ public cancelBooking(booking: BookingResponseDto) {
if ( !dateStr ){ this.calendarService.calendarControllerCancelBooking(
return ""; booking.id,
{
canceledReason: 'customer_cancelled',
},
).subscribe({
next: () => {
// this.eventBus.emit(EventType.CALENDAR_VIEW_BOOKING_CANCELLED, '');
this.bookings.reload();
},
error: () => {
// todo: handle error
}
});
}
formatDateTime(dateStr?: string | Date | null) {
if (!dateStr) {
return '';
} }
return format(new Date(dateStr),'yyyy-MM-dd HH:mm'); return format(new Date(dateStr), 'yyyy-MM-dd HH:mm');
} }
protected readonly format = format; protected readonly format = format;
protected readonly SvgIcons = SvgIcons;
} }

View File

@@ -43,9 +43,9 @@ export class SingleEventDashboardEventActivation {
if (eventException) { if (eventException) {
payload = { payload = {
...eventException, ...eventException,
originalStartTime: new Date(eventException.originalStartTime), originalStartTime: new Date(eventException.originalStartTime),
newStartTime: eventException.newStartTime ? new Date(eventException.newStartTime) : undefined, newStartTime: eventException.newStartTime ? new Date(eventException.newStartTime) : undefined,
newEndTime: eventException.newEndTime ? new Date(eventException.newEndTime) :undefined, newEndTime: eventException.newEndTime ? new Date(eventException.newEndTime) : undefined,
isCancelled: !activated, isCancelled: !activated,
}; };
} else { } else {
@@ -55,7 +55,7 @@ export class SingleEventDashboardEventActivation {
}; };
} }
this.calendarService.applyException(eventId, payload ).subscribe( this.calendarService.applyException(eventId, payload).subscribe(
{ {
next: () => { next: () => {
this.eventBus.emit(EventType.CALENDAR_VIEW_EVENT_SAVED, 'Event saved') this.eventBus.emit(EventType.CALENDAR_VIEW_EVENT_SAVED, 'Event saved')
@@ -77,7 +77,7 @@ export class SingleEventDashboardEventActivation {
} }
protected closeDialog() { protected closeDialog() {
this.eventBus.emit(EventType.CALENDAR_VIEW_DIALOG_CLOSED, 'Event saved') this.eventBus.emit(EventType.CALENDAR_VIEW_EVENT_DASHBOARD, 'Event saved')
} }
} }

View File

@@ -22,12 +22,12 @@ export class SingleEventDashboardEventEdit {
* proxy to ready event from form to parent * proxy to ready event from form to parent
*/ */
protected triggerAction(action: ReadyType) { protected triggerAction(action: ReadyType) {
if ( action == 'save-event-success'){ if (action == 'save-event-success') {
this.eventBus.emit(EventType.CALENDAR_VIEW_EVENT_SAVED,''); this.eventBus.emit(EventType.CALENDAR_VIEW_EVENT_SAVED, '');
}else if ( action == 'save-event-failed'){ } else if (action == 'save-event-failed') {
this.eventBus.emit(EventType.CALENDAR_VIEW_DIALOG_CLOSED,''); this.eventBus.emit(EventType.CALENDAR_VIEW_DIALOG_CLOSED, '');
}else{ } else {
this.eventBus.emit(EventType.CALENDAR_VIEW_DIALOG_CLOSED,''); this.eventBus.emit(EventType.CALENDAR_VIEW_EVENT_DASHBOARD, '');
} }
} }
} }

View File

@@ -28,65 +28,65 @@ export class SingleEventDashboard {
constructor() { constructor() {
effect(() => { effect(() => {
this.cards.set( [ this.cards.set([
{
buttonTitle: 'Szerkesztés',
title: 'Szerkesztés',
svgIcon: SvgIcons.heorPencilSquare,
description: 'Az esemény módosítása',
workflow: 'event_edit',
},
this.event()?.isCancelled ?
{ {
buttonTitle: 'Aktiválás', buttonTitle: 'Szerkesztés',
title: 'Előfordulás aktiválása', title: 'Szerkesztés',
svgIcon: SvgIcons.heroPlay, svgIcon: SvgIcons.heorPencilSquare,
description: 'Az esemény ezen előfordulásának aktiválása', description: 'Az esemény módosítása',
workflow: 'event_activate', workflow: 'event_edit',
} : },
{
buttonTitle: 'Lemondás', this.event()?.isCancelled ?
title: 'Előfordulás lemondása',
svgIcon: SvgIcons.heroXcircle, {
description: 'Az esemény ezen előfordulásának lemondása', buttonTitle: 'Aktiválás',
workflow: 'event_cancel', title: 'Előfordulás aktiválása',
}, svgIcon: SvgIcons.heroPlay,
{ description: 'Az esemény ezen előfordulásának aktiválása',
buttonTitle: 'Törlés', workflow: 'event_activate',
title: 'Esemény törlése', } :
svgIcon: SvgIcons.heroTrash, {
description: 'Az esemény törlése', buttonTitle: 'Lemondás',
workflow: 'event_delete', title: 'Előfordulás lemondása',
}, svgIcon: SvgIcons.heroXcircle,
{ description: 'Az esemény ezen előfordulásának lemondása',
buttonTitle: 'Megnézem', workflow: 'event_cancel',
title: 'Foglalások', },
svgIcon: SvgIcons.heroUserGroup, {
description: 'Foglalások megtekintése', buttonTitle: 'Törlés',
workflow: 'booking_list', title: 'Esemény törlése',
}, svgIcon: SvgIcons.heroTrash,
{ description: 'Az esemény törlése',
buttonTitle: 'Bejelentkezés', workflow: 'event_delete',
title: 'Időpont foglalás', },
svgIcon: SvgIcons.heroUserPlus, {
description: 'Időpont foglalása eseményre', buttonTitle: 'Megnézem',
workflow: 'booking_create', title: 'Foglalások',
}, svgIcon: SvgIcons.heroUserGroup,
{ description: 'Foglalások megtekintése',
buttonTitle: 'Lemondás', workflow: 'booking_list',
title: 'Lemondás', },
svgIcon: SvgIcons.heroUserMinus, {
description: 'Az időpont lemondása', buttonTitle: 'Bejelentkezés',
workflow: 'booking_cancel', title: 'Időpont foglalás',
}, svgIcon: SvgIcons.heroUserPlus,
]); description: 'Időpont foglalása eseményre',
workflow: 'booking_create',
},
{
buttonTitle: 'Lemondás',
title: 'Lemondás',
svgIcon: SvgIcons.heroUserMinus,
description: 'Az időpont lemondása',
workflow: 'booking_cancel',
},
]);
}); });
} }
onCardAction (action: WORKFLOW_TYPE) { onCardAction(action: WORKFLOW_TYPE) {
if ( action ){ if (action) {
this.action.emit(action); this.action.emit(action);
} }
console.info("card action", action); console.info("card action", action);

View File

@@ -1,115 +1,108 @@
<!-- Generated by the CLI --> <!-- Generated by the CLI -->
<div class=""> <div class="">
<h2 class="card-title text-3xl"> <h2 class="card-title text-3xl">
Esemény {{ isEditMode ? 'szerkesztése' : 'létrehozása' }} Esemény {{ isEditMode ? 'szerkesztése' : 'létrehozása' }}
</h2> </h2>
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="space-y-4 mt-4"> <form [formGroup]="form" (ngSubmit)="onSubmit()" class="space-y-4 mt-4">
<div class="form-control"><label class="label"><span class="label-text">Megnevezés</span></label> <div class="form-control"><label class="label"><span class="label-text">Megnevezés</span></label>
<input [class.input-error]="title?.invalid && title?.touched" type="text" formControlName="title" class="input input-bordered w-full" /> <input [class.input-error]="title?.invalid && title?.touched" type="text" formControlName="title"
</div> class="input input-bordered w-full" />
</div>
<div class="form-control"><label class="label"><span class="label-text">Esemény típus</span></label> <div class="form-control"><label class="label"><span class="label-text">Esemény típus</span></label>
<select class="select w-full" <select class="select w-full" formControlName="eventTypeId"
formControlName="eventTypeId" [class.input-error]="eventType?.invalid && eventType?.touched">
[class.input-error]="eventType?.invalid && eventType?.touched" <option disabled selected>Pick a color</option>
> @for (eventType of eventTypes(); track eventType.id) {
<option disabled selected>Pick a color</option> <option [value]="eventType.id">{{ eventType.name }}</option>
@for (eventType of eventTypes(); track eventType.id) {
<option [value]="eventType.id">{{ eventType.name }}</option>
}
</select>
</div>
<div class="form-control"><label class="label"><span class="label-text">Leírás</span></label>
<input [class.input-error]="description?.invalid && description?.touched" type="text" formControlName="description" class="input input-bordered w-full" /></div>
<div class="form-control"><label class="label"><span class="label-text">Kezdő időpont</span></label>
<input [class.input-error]="startTime?.invalid && startTime?.touched" type="datetime-local" formControlName="startTime" class="input input-bordered w-full" /></div>
<div class="form-control"><label class="label"><span class="label-text">Befejezési időpont</span></label>
<input [class.input-error]="endTime?.invalid && endTime?.touched" type="datetime-local" formControlName="endTime" class="input input-bordered w-full" /></div>
<div class="form-control"><label class="label cursor-pointer justify-start gap-4">
<span class="label-text">Ismétlődő</span>
<input type="checkbox" formControlName="isRecurring" class="checkbox" />
</label></div>
@if (isRecurring?.value === true) {
<div formGroupName="recurrenceRule" class="space-y-4 p-4 border border-base-300 rounded-lg">
<h3 class="text-lg font-semibold">Ismétlődés</h3>
<div class="form-control">
<label class="label"><span class="label-text">Gyakoriság</span></label>
<select class="select w-full"
formControlName="frequency"
[class.select-error]="frequency?.invalid && frequency?.touched">
<option disabled selected>Válassz gyakoriságot</option>
@for (frequencyOption of frequencyOptions; track frequencyOption) {
<option [value]="frequencyOption.frequency">{{ frequencyOption.label }}</option>
}
</select>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Intervallum</span></label>
<input type="number"
formControlName="interval"
class="input input-bordered w-full"
[class.input-error]="interval?.invalid && interval?.touched" />
</div>
<div class="form-control">
<label class="label"><span class="label-text">Napok</span></label>
<div class="flex flex-wrap gap-4">
@for (day of weekDayOptions; track day.value) {
<label class="label cursor-pointer justify-start gap-2">
<input type="checkbox"
[value]="day.value"
[checked]="isDayChecked(day.value)"
(change)="onByDayChange($event)"
class="checkbox" />
<span class="label-text">{{ day.label }}</span>
</label>
}
</div>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Hónap napja</span></label>
<input type="text"
formControlName="byMonthDay"
class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">Vesszővel elválasztott lista (pl. 1,15,31)</span>
</div>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Hónap</span></label>
<input type="text"
formControlName="byMonth"
class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">Vesszővel elválasztott lista (pl. 1,2,3)</span>
</div>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Ismétlődés vége</span></label>
<input type="date"
formControlName="endDate"
class="input input-bordered w-full" />
</div>
</div>
} }
</select>
</div>
<div class="card-actions justify-end mt-6"> <div class="form-control"><label class="label"><span class="label-text">Leírás</span></label>
<a class="btn btn-ghost" (click)="doReady()">Mégse</a> <input [class.input-error]="description?.invalid && description?.touched" type="text"
<button type="submit" class="btn btn-primary" [disabled]="form.invalid"> formControlName="description" class="input input-bordered w-full" />
{{ isEditMode ? 'Mentés' : 'Létrezhozás' }} </div>
</button>
<div class="form-control"><label class="label"><span class="label-text">Kezdő időpont</span></label>
<input [class.input-error]="startTime?.invalid && startTime?.touched" type="datetime-local"
formControlName="startTime" class="input input-bordered w-full" />
</div>
<div class="form-control"><label class="label"><span class="label-text">Befejezési időpont</span></label>
<input [class.input-error]="endTime?.invalid && endTime?.touched" type="datetime-local" formControlName="endTime"
class="input input-bordered w-full" />
</div>
<div class="form-control"><label class="label cursor-pointer justify-start gap-4">
<span class="label-text">Ismétlődő</span>
<input type="checkbox" formControlName="isRecurring" class="checkbox" />
</label></div>
@if (isRecurring?.value === true) {
<div formGroupName="recurrenceRule" class="space-y-4 p-4 border border-base-300 rounded-lg">
<h3 class="text-lg font-semibold">Ismétlődés</h3>
<div class="form-control">
<label class="label"><span class="label-text">Gyakoriság</span></label>
<select class="select w-full" formControlName="frequency"
[class.select-error]="frequency?.invalid && frequency?.touched">
<option disabled selected>Válassz gyakoriságot</option>
@for (frequencyOption of frequencyOptions; track frequencyOption) {
<option [value]="frequencyOption.frequency">{{ frequencyOption.label }}</option>
}
</select>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Intervallum</span></label>
<input type="number" formControlName="interval" class="input input-bordered w-full"
[class.input-error]="interval?.invalid && interval?.touched" />
</div>
<div class="form-control">
<label class="label"><span class="label-text">Napok</span></label>
<div class="flex flex-wrap gap-4">
@for (day of weekDayOptions; track day.value) {
<label class="label cursor-pointer justify-start gap-2">
<input type="checkbox" [value]="day.value" [checked]="isDayChecked(day.value)"
(change)="onByDayChange($event)" class="checkbox" />
<span class="label-text">{{ day.label }}</span>
</label>
}
</div> </div>
</form> </div>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Hónap napja</span></label>
<input type="text" formControlName="byMonthDay" class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">Vesszővel elválasztott lista (pl. 1,15,31)</span>
</div>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Hónap</span></label>
<input type="text" formControlName="byMonth" class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">Vesszővel elválasztott lista (pl. 1,2,3)</span>
</div>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Ismétlődés vége</span></label>
<input type="date" formControlName="endDate" class="input input-bordered w-full" />
</div>
</div>
}
<div class="card-actions justify-end mt-6">
<button type="submit" class="btn btn-primary" [disabled]="form.invalid">
{{ isEditMode ? 'Mentés' : 'Létrezhozás' }}
</button>
<a class="btn btn-primary" (click)="doReady()">Mégse</a>
</div>
</form>
</div>

View File

@@ -39,9 +39,24 @@ 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"> 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" /> <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> </svg>
`; `;
public static heroMinusCircle = `<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="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
`;
public static heroBadgeCircle = `<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="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 0 1-1.043 3.296 3.745 3.745 0 0 1-3.296 1.043A3.745 3.745 0 0 1 12 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 0 1-3.296-1.043 3.745 3.745 0 0 1-1.043-3.296A3.745 3.745 0 0 1 3 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 0 1 1.043-3.296 3.746 3.746 0 0 1 3.296-1.043A3.746 3.746 0 0 1 12 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 0 1 3.296 1.043 3.746 3.746 0 0 1 1.043 3.296Z" />
</svg>
`;
public static heroCheckCircle = `
<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="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
`;
} }

View File

@@ -1,5 +1,5 @@
export interface AppConfig{ export interface AppConfig {
apiUrl: string; apiUrl: string;
apiBaseUrl: string; apiBaseUrl: string;
} }
@@ -19,8 +19,11 @@ export enum EventType {
USER_LOGGED_IN = 'USER_LOGGED_IN', USER_LOGGED_IN = 'USER_LOGGED_IN',
ORDER_CREATED = 'ORDER_CREATED', ORDER_CREATED = 'ORDER_CREATED',
THEME_CHANGED = 'THEME_CHANGED', THEME_CHANGED = 'THEME_CHANGED',
CALENDAR_VIEW_EVENT_DASHBOARD = 'CALENDAR_VIEW_EVENT_DASHBOARD',
CALENDAR_VIEW_EVENT_SAVED = 'CALENDAR_VIEW_EVENT_SAVED', CALENDAR_VIEW_EVENT_SAVED = 'CALENDAR_VIEW_EVENT_SAVED',
CALENDAR_VIEW_DIALOG_CLOSED = 'CALENDAR_VIEW_DIALOG_CLOSED' CALENDAR_VIEW_DIALOG_CLOSED = 'CALENDAR_VIEW_DIALOG_CLOSED',
CALENDAR_VIEW_BOOKING_CANCELLED = 'CALENDAR_VIEW_BOOKING_CANCELLED',
CALENDAR_VIEW_CLOSE_DIALOG_AND_RELOAD = 'CALENDAR_VIEW_CLOSE_DIALOG_AND_RELOAD'
} }
@@ -30,6 +33,10 @@ export interface EventMap {
[EventType.ORDER_CREATED]: { orderId: number }; [EventType.ORDER_CREATED]: { orderId: number };
[EventType.CALENDAR_VIEW_EVENT_SAVED]: string; [EventType.CALENDAR_VIEW_EVENT_SAVED]: string;
[EventType.CALENDAR_VIEW_DIALOG_CLOSED]: string; [EventType.CALENDAR_VIEW_DIALOG_CLOSED]: string;
[EventType.CALENDAR_VIEW_DIALOG_CLOSED]: string;
[EventType.CALENDAR_VIEW_BOOKING_CANCELLED]: string;
[EventType.CALENDAR_VIEW_CLOSE_DIALOG_AND_RELOAD]: string;
[EventType.CALENDAR_VIEW_EVENT_DASHBOARD]: string;
} }
export interface AppEvent<T = any> { export interface AppEvent<T = any> {

8
dius.txt Normal file
View File

@@ -0,0 +1,8 @@
NAiP
file:///Users/rschneider/Library/Fonts/FiraCodeNerdFontMono-Light.ttf#postscript-name=FiraCodeNFM-Light
file:///Users/rschneider/Library/Fonts/FiraCodeNerdFontMono-Regular.ttf#postscript-name=FiraCodeNFM-Reg
file:///Users/rschneider/Library/Fonts/FiraCodeNerdFontMono-Retina.ttf#postscript-name=FiraCodeNFM-Ret
file:///Users/rschneider/Library/Fonts/FiraCodeNerdFontMono-Medium.ttf#postscript-name=FiraCodeNFM-Med
file:///Users/rschneider/Library/Fonts/FiraCodeNerdFontMono-SemiBold.ttf#postscript-name=FiraCodeNFM-SemBd
file:///Users/rschneider/Library/Fonts/FiraCodeNerdFontMono-Bold.ttf#postscript-name=FiraCodeNFM-Bold

View File

@@ -31,7 +31,7 @@ import { CalenderControllerGetBookingResponse } from './dto/booking.response.dto
@UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.Admin) @Roles(Role.Admin)
export class CalendarController { export class CalendarController {
constructor(private readonly calendarService: CalendarService) {} constructor(private readonly calendarService: CalendarService) { }
@Get() @Get()
getCalendarEvents(@Query() getCalendarDto: GetCalendarDto) { getCalendarEvents(@Query() getCalendarDto: GetCalendarDto) {
@@ -110,6 +110,7 @@ export class CalendarController {
@ApiCreatedResponse({ type: CalenderControllerGetBookingResponse }) // Removed <Booking> generic for cleaner syntax unless defined elsewhere @ApiCreatedResponse({ type: CalenderControllerGetBookingResponse }) // Removed <Booking> generic for cleaner syntax unless defined elsewhere
// Ensure ValidationPipe is enabled with transform: true (Global or Method scoped) // Ensure ValidationPipe is enabled with transform: true (Global or Method scoped)
// @UsePipes(new ValidationPipe({ transform: true })) // @UsePipes(new ValidationPipe({ transform: true }))
@Roles(Role.Admin, Role.User)
async getBookings( async getBookings(
@User() user: types.AppUser, @User() user: types.AppUser,
@Param('eventId', ParseIntPipe) eventId: number, @Param('eventId', ParseIntPipe) eventId: number,
@@ -117,7 +118,7 @@ export class CalendarController {
@Query() calendarGetBookingDto: CalendarGetBookingDto, @Query() calendarGetBookingDto: CalendarGetBookingDto,
) { ) {
return this.calendarService.getBookings( return this.calendarService.getBookings(
user.user!.userId, user,
eventId, eventId,
calendarGetBookingDto, calendarGetBookingDto,
); );

View File

@@ -28,6 +28,8 @@ import {
BookingResponseDto, BookingResponseDto,
CalenderControllerGetBookingResponse, CalenderControllerGetBookingResponse,
} from './dto/booking.response.dto'; } from './dto/booking.response.dto';
import { AppUser } from 'src/types';
import { Role } from '../auth/role.enum';
// --- Type-Safe Maps --- // --- Type-Safe Maps ---
const frequencyMap: Record<string, RRule.Frequency> = { const frequencyMap: Record<string, RRule.Frequency> = {
@@ -82,7 +84,7 @@ export class CalendarService {
private readonly eventExceptionRepository: Repository<EventException>, private readonly eventExceptionRepository: Repository<EventException>,
@InjectRepository(Booking) @InjectRepository(Booking)
private readonly bookingRepository: Repository<Booking>, private readonly bookingRepository: Repository<Booking>,
) {} ) { }
async getEventsInRange( async getEventsInRange(
startDate: Date, startDate: Date,
@@ -606,7 +608,7 @@ export class CalendarService {
} }
async getBookings( async getBookings(
userId: number, user: AppUser,
eventId: number, eventId: number,
queryParams: CalendarGetBookingDto, queryParams: CalendarGetBookingDto,
): Promise<CalenderControllerGetBookingResponse> { ): Promise<CalenderControllerGetBookingResponse> {
@@ -621,6 +623,13 @@ export class CalendarService {
['createdAt']: order!, ['createdAt']: order!,
}, },
}; };
if (!user.user?.roles.includes(Role.Admin)) {
findOptions.where = {
...findOptions.where,
userId: user.user!.userId,
};
}
const paginated = limit > 0; const paginated = limit > 0;
const [data, totalItems] = const [data, totalItems] =
await this.bookingRepository.findAndCount(findOptions); await this.bookingRepository.findAndCount(findOptions);

View File

@@ -32,6 +32,9 @@ export class BookingResponseDto {
}) })
user?: UserResponseDto; user?: UserResponseDto;
@ApiProperty({ required: false })
status?: string;
constructor(booking: Booking) { constructor(booking: Booking) {
this.id = Number(booking.id); // Handle BigInt conversion this.id = Number(booking.id); // Handle BigInt conversion
this.eventId = Number(booking.eventId); this.eventId = Number(booking.eventId);
@@ -46,6 +49,14 @@ export class BookingResponseDto {
// Assuming User entity has firstName/lastName // Assuming User entity has firstName/lastName
this.user = new UserResponseDto(booking.user); this.user = new UserResponseDto(booking.user);
} }
this.status = 'active';
if (booking.canceledAt) {
if (booking.canceledReason == 'customer_cancelled') {
this.status = 'customer_cancelled';
} else {
this.status = 'cancelled';
}
}
} }
} }

View File

@@ -1,14 +1,12 @@
import { import {
IsInt,
IsNotEmpty,
IsOptional, IsOptional,
IsString, IsString,
MaxLength, MaxLength,
} from 'class-validator'; } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CancelBookingDto { export class CancelBookingDto {
@ApiProperty()
@IsOptional() @IsOptional()
@IsString() @IsString()
@MaxLength(50) @MaxLength(50)