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 {
canceledReason: string;
}
export interface UserResponseDto {
@@ -94,6 +95,7 @@ export interface BookingResponseDto {
canceledAt: string | null;
createdAt: string | null;
user: UserResponseDto;
status?: string;
}
export interface PaginationResponseMetaDto {

View File

@@ -137,9 +137,9 @@ export class CalendarService {
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?: 'response', options?: RequestOptions<'json'>): Observable<HttpResponse<any>>;
calendarControllerCancelBooking(bookingId: number, cancelBookingDto: CancelBookingDto, observe?: 'events', options?: RequestOptions<'json'>): Observable<HttpEvent<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<CalenderControllerGetBookingResponse>>;
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> {
const url = `${this.basePath}/api/calendar/bookings/${bookingId}/cancel`;

View File

@@ -5,26 +5,22 @@
@if (workflow() == 'event_create') {
<rs-daisy-modal [isOpen]="true" (closeClick)="closeDialog()" [modalBoxStyleClass]="'max-w-none w-2xl'">
<app-create-event-form (ready)="closeDialog()" [date]="selectedDate()"
[id]="undefined"></app-create-event-form>
<app-create-event-form (ready)="closeDialog()" [date]="selectedDate()" [id]="undefined"></app-create-event-form>
</rs-daisy-modal>
}
@if (workflow() == 'event_dashboard' && selectedEvent()) {
<rs-daisy-modal [isOpen]="true" (closeClick)="closeDialog()" [modalBoxStyleClass]="'max-w-none w-2xl'">
<app-single-event-dashboard [event]="selectedEvent()"
(action)="setWorkFlow($event)"
></app-single-event-dashboard>
</rs-daisy-modal>
<rs-daisy-modal [isOpen]="true" (closeClick)="closeDialog()" [modalBoxStyleClass]="'max-w-none w-2xl'">
<app-single-event-dashboard [event]="selectedEvent()" (action)="setWorkFlow($event)"></app-single-event-dashboard>
</rs-daisy-modal>
}
@for (dialogDefinition of dialogs; track dialogDefinition) {
@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>
}
@if (dialogDefinition.isRendered()) {
<rs-daisy-modal [isOpen]="true" (closeClick)="openDashboard()" [modalBoxStyleClass]="'max-w-none w-2xl'">
<ng-container
*ngComponentOutlet="dialogDefinition.component; inputs: dialogDefinition.componentInputs ? dialogDefinition.componentInputs() : {}; "></ng-container>
</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())
.subscribe({
next: (_) => {
this.closeDialog();
this.calendarComponent?.getApi().refetchEvents();
}
})
this.eventBus.on(EventType.CALENDAR_VIEW_EVENT_DASHBOARD)
.pipe(takeUntilDestroyed())
.subscribe({
next: (_) => {
this.openDashboard();
}
})
this.calendarOptions = {
plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin],
@@ -237,16 +247,16 @@ export class CalendarView {
}),
)
.subscribe({
next: (events) => {
console.info('calendar events', events);
successCallback(events);
}
,
error: (error) => {
console.error('Error fetching events', error);
failureCallback(error);
},
next: (events) => {
console.info('calendar events', events);
successCallback(events);
}
,
error: (error) => {
console.error('Error fetching events', error);
failureCallback(error);
},
},
);
}
@@ -254,7 +264,9 @@ export class CalendarView {
closeDialog() {
this.workflow.set('NO_DIALOG');
}
openDashboard() {
this.workflow.set('event_dashboard');
}
/**
* Set dashboard workflow
* @param workflowType

View File

@@ -2,37 +2,52 @@
Foglalások
</app-heading>
@if (bookings.isLoading()) {
<div>loading...</div>
<div>loading...</div>
} @else {
@if (bookings.value()?.data?.length) {
<div class="overflow-x-auto">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Foglalás dátuma</th>
<th>Foglalt helyek száma</th>
<th></th>
</tr>
</thead>
<tbody>
@for (booking of bookings.value()?.data; track booking) {
<tr>
<td>{{formatDateTime(booking.createdAt)}}</td>
<td>{{booking.reservedSeatsCount}}</td>
<td><rs-daisy-button [variant]="'error'">
Lemondás
</rs-daisy-button></td>
</tr>
@if (bookings.value()?.data?.length) {
<div class="overflow-x-auto">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Foglalás dátuma</th>
<th>Foglalt helyek száma</th>
<th>Státusz</th>
<th></th>
</tr>
</thead>
<tbody>
@for (booking of bookings.value()?.data; track booking) {
<tr>
<td>{{formatDateTime(booking.createdAt)}}</td>
<td>{{booking.reservedSeatsCount}}</td>
<td>
@switch (booking.status){
@case ('active'){
<span [outerHTML]="SvgIcons.heroCheckCircle | safeHtml"></span>
}
</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
}
@case ('customer_cancelled'){
<span [outerHTML]="SvgIcons.heroMinusCircle | safeHtml"></span>
}
}
</td>
<td>
@if (booking.status == 'active') {
<rs-daisy-button [variant]="'error'" (clickEvent)="cancelBooking(booking)">
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 { Heading } from '../../../../../components/heading/heading';
import { format } from 'date-fns';
import { EventType } from '../../../../../../types';
import { SvgIcons } from '../../../../../svg-icons';
import { SafeHtmlPipe } from "../../../../../pipes/safe-html-pipe";
@Component({
selector: 'app-single-event-booking-list',
@@ -14,6 +17,7 @@ import { format } from 'date-fns';
Pagination,
Heading,
Button,
SafeHtmlPipe
],
templateUrl: './single-event-booking-list.html',
styleUrl: './single-event-booking-list.css',
@@ -30,57 +34,64 @@ export class SingleEventBookingList {
pageCount = computed(() => {
// return this.bookings.value()?.pageCount || 1;
return 1;
})
});
bookings = rxResource(
{
params: () => ({
page: this.activePage()
page: this.activePage(),
}),
stream: ({params}) => {
stream: ({ params }) => {
// console.info("loading resource", params);
//
// 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"))
if (!this.event()) {
return throwError(() => new Error('no event'));
}
const event = this.event()!;
return this.calendarService.calendarControllerGetBookings(
event.id,
new Date(event.startTime).toISOString()
)
new Date(event.startTime).toISOString(),
params.page,
this.pageSize(),
);
},
}
)
},
);
protected paginate($event: number) {
console.info("paginated to ", $event)
console.info('paginated to ', $event);
this.activePage.set($event);
}
formatDateTime( dateStr?: string|Date|null){
if ( !dateStr ){
return "";
public cancelBooking(booking: BookingResponseDto) {
this.calendarService.calendarControllerCancelBooking(
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 SvgIcons = SvgIcons;
}

View File

@@ -43,9 +43,9 @@ export class SingleEventDashboardEventActivation {
if (eventException) {
payload = {
...eventException,
originalStartTime: new Date(eventException.originalStartTime),
originalStartTime: new Date(eventException.originalStartTime),
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,
};
} else {
@@ -55,7 +55,7 @@ export class SingleEventDashboardEventActivation {
};
}
this.calendarService.applyException(eventId, payload ).subscribe(
this.calendarService.applyException(eventId, payload).subscribe(
{
next: () => {
this.eventBus.emit(EventType.CALENDAR_VIEW_EVENT_SAVED, 'Event saved')
@@ -77,7 +77,7 @@ export class SingleEventDashboardEventActivation {
}
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
*/
protected triggerAction(action: ReadyType) {
if ( action == 'save-event-success'){
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,'');
if (action == 'save-event-success') {
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_EVENT_DASHBOARD, '');
}
}
}

View File

@@ -28,65 +28,65 @@ export class SingleEventDashboard {
constructor() {
effect(() => {
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 ?
this.cards.set([
{
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',
workflow: 'event_activate',
} :
{
buttonTitle: 'Lemondás',
title: 'Előfordulás lemondása',
svgIcon: SvgIcons.heroXcircle,
description: 'Az esemény ezen előfordulásának lemondása',
workflow: 'event_cancel',
},
{
buttonTitle: 'Törlés',
title: 'Esemény törlése',
svgIcon: SvgIcons.heroTrash,
description: 'Az esemény törlése',
workflow: 'event_delete',
},
{
buttonTitle: 'Megnézem',
title: 'Foglalások',
svgIcon: SvgIcons.heroUserGroup,
description: 'Foglalások megtekintése',
workflow: 'booking_list',
},
{
buttonTitle: 'Bejelentkezés',
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',
},
]);
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',
title: 'Előfordulás aktiválása',
svgIcon: SvgIcons.heroPlay,
description: 'Az esemény ezen előfordulásának aktiválása',
workflow: 'event_activate',
} :
{
buttonTitle: 'Lemondás',
title: 'Előfordulás lemondása',
svgIcon: SvgIcons.heroXcircle,
description: 'Az esemény ezen előfordulásának lemondása',
workflow: 'event_cancel',
},
{
buttonTitle: 'Törlés',
title: 'Esemény törlése',
svgIcon: SvgIcons.heroTrash,
description: 'Az esemény törlése',
workflow: 'event_delete',
},
{
buttonTitle: 'Megnézem',
title: 'Foglalások',
svgIcon: SvgIcons.heroUserGroup,
description: 'Foglalások megtekintése',
workflow: 'booking_list',
},
{
buttonTitle: 'Bejelentkezés',
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) {
if ( action ){
onCardAction(action: WORKFLOW_TYPE) {
if (action) {
this.action.emit(action);
}
console.info("card action", action);

View File

@@ -1,115 +1,108 @@
<!-- Generated by the CLI -->
<div class="">
<h2 class="card-title text-3xl">
Esemény {{ isEditMode ? 'szerkesztése' : 'létrehozása' }}
</h2>
<h2 class="card-title text-3xl">
Esemény {{ isEditMode ? 'szerkesztése' : 'létrehozása' }}
</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>
<input [class.input-error]="title?.invalid && title?.touched" type="text" formControlName="title" class="input input-bordered w-full" />
</div>
<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" />
</div>
<div class="form-control"><label class="label"><span class="label-text">Esemény típus</span></label>
<select class="select w-full"
formControlName="eventTypeId"
[class.input-error]="eventType?.invalid && eventType?.touched"
>
<option disabled selected>Pick a color</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>
<div class="form-control"><label class="label"><span class="label-text">Esemény típus</span></label>
<select class="select w-full" formControlName="eventTypeId"
[class.input-error]="eventType?.invalid && eventType?.touched">
<option disabled selected>Pick a color</option>
@for (eventType of eventTypes(); track eventType.id) {
<option [value]="eventType.id">{{ eventType.name }}</option>
}
</select>
</div>
<div class="card-actions justify-end mt-6">
<a class="btn btn-ghost" (click)="doReady()">Mégse</a>
<button type="submit" class="btn btn-primary" [disabled]="form.invalid">
{{ isEditMode ? 'Mentés' : 'Létrezhozás' }}
</button>
<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>
</form>
</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" />
</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" />
</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;
apiBaseUrl: string;
}
@@ -19,8 +19,11 @@ export enum EventType {
USER_LOGGED_IN = 'USER_LOGGED_IN',
ORDER_CREATED = 'ORDER_CREATED',
THEME_CHANGED = 'THEME_CHANGED',
CALENDAR_VIEW_EVENT_DASHBOARD = 'CALENDAR_VIEW_EVENT_DASHBOARD',
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.CALENDAR_VIEW_EVENT_SAVED]: 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> {

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

View File

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

View File

@@ -32,6 +32,9 @@ export class BookingResponseDto {
})
user?: UserResponseDto;
@ApiProperty({ required: false })
status?: string;
constructor(booking: Booking) {
this.id = Number(booking.id); // Handle BigInt conversion
this.eventId = Number(booking.eventId);
@@ -46,6 +49,14 @@ export class BookingResponseDto {
// Assuming User entity has firstName/lastName
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 {
IsInt,
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CancelBookingDto {
@ApiProperty()
@IsOptional()
@IsString()
@MaxLength(50)