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)"
></app-single-event-dashboard>
</rs-daisy-modal> </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],
@@ -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

@@ -12,6 +12,7 @@
<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>Státusz</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@@ -20,9 +21,23 @@
<tr> <tr>
<td>{{formatDateTime(booking.createdAt)}}</td> <td>{{formatDateTime(booking.createdAt)}}</td>
<td>{{booking.reservedSeatsCount}}</td> <td>{{booking.reservedSeatsCount}}</td>
<td><rs-daisy-button [variant]="'error'"> <td>
@switch (booking.status){
@case ('active'){
<span [outerHTML]="SvgIcons.heroCheckCircle | safeHtml"></span>
}
@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 Lemondás
</rs-daisy-button></td> </rs-daisy-button>
}
</td>
</tr> </tr>
} }
</tbody> </tbody>

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);
//
// 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()) { if (!this.event()) {
return throwError( () => new Error("no 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);
} }
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) { formatDateTime(dateStr?: string | Date | null) {
if (!dateStr) { if (!dateStr) {
return ""; 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

@@ -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

@@ -27,7 +27,7 @@ export class SingleEventDashboardEventEdit {
} 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

@@ -7,14 +7,13 @@
<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"
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> <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> <option disabled selected>Pick a color</option>
@for (eventType of eventTypes(); track eventType.id) { @for (eventType of eventTypes(); track eventType.id) {
<option [value]="eventType.id">{{ eventType.name }}</option> <option [value]="eventType.id">{{ eventType.name }}</option>
@@ -23,13 +22,19 @@
</div> </div>
<div class="form-control"><label class="label"><span class="label-text">Leírás</span></label> <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> <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> <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> <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> <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> <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"> <div class="form-control"><label class="label cursor-pointer justify-start gap-4">
<span class="label-text">Ismétlődő</span> <span class="label-text">Ismétlődő</span>
@@ -42,8 +47,7 @@
<div class="form-control"> <div class="form-control">
<label class="label"><span class="label-text">Gyakoriság</span></label> <label class="label"><span class="label-text">Gyakoriság</span></label>
<select class="select w-full" <select class="select w-full" formControlName="frequency"
formControlName="frequency"
[class.select-error]="frequency?.invalid && frequency?.touched"> [class.select-error]="frequency?.invalid && frequency?.touched">
<option disabled selected>Válassz gyakoriságot</option> <option disabled selected>Válassz gyakoriságot</option>
@for (frequencyOption of frequencyOptions; track frequencyOption) { @for (frequencyOption of frequencyOptions; track frequencyOption) {
@@ -54,9 +58,7 @@
<div class="form-control"> <div class="form-control">
<label class="label"><span class="label-text">Intervallum</span></label> <label class="label"><span class="label-text">Intervallum</span></label>
<input type="number" <input type="number" formControlName="interval" class="input input-bordered w-full"
formControlName="interval"
class="input input-bordered w-full"
[class.input-error]="interval?.invalid && interval?.touched" /> [class.input-error]="interval?.invalid && interval?.touched" />
</div> </div>
@@ -65,11 +67,8 @@
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
@for (day of weekDayOptions; track day.value) { @for (day of weekDayOptions; track day.value) {
<label class="label cursor-pointer justify-start gap-2"> <label class="label cursor-pointer justify-start gap-2">
<input type="checkbox" <input type="checkbox" [value]="day.value" [checked]="isDayChecked(day.value)"
[value]="day.value" (change)="onByDayChange($event)" class="checkbox" />
[checked]="isDayChecked(day.value)"
(change)="onByDayChange($event)"
class="checkbox" />
<span class="label-text">{{ day.label }}</span> <span class="label-text">{{ day.label }}</span>
</label> </label>
} }
@@ -78,9 +77,7 @@
<div class="form-control"> <div class="form-control">
<label class="label"><span class="label-text">Hónap napja</span></label> <label class="label"><span class="label-text">Hónap napja</span></label>
<input type="text" <input type="text" formControlName="byMonthDay" class="input input-bordered w-full" />
formControlName="byMonthDay"
class="input input-bordered w-full" />
<div class="label"> <div class="label">
<span class="label-text-alt">Vesszővel elválasztott lista (pl. 1,15,31)</span> <span class="label-text-alt">Vesszővel elválasztott lista (pl. 1,15,31)</span>
</div> </div>
@@ -88,9 +85,7 @@
<div class="form-control"> <div class="form-control">
<label class="label"><span class="label-text">Hónap</span></label> <label class="label"><span class="label-text">Hónap</span></label>
<input type="text" <input type="text" formControlName="byMonth" class="input input-bordered w-full" />
formControlName="byMonth"
class="input input-bordered w-full" />
<div class="label"> <div class="label">
<span class="label-text-alt">Vesszővel elválasztott lista (pl. 1,2,3)</span> <span class="label-text-alt">Vesszővel elválasztott lista (pl. 1,2,3)</span>
</div> </div>
@@ -98,18 +93,16 @@
<div class="form-control"> <div class="form-control">
<label class="label"><span class="label-text">Ismétlődés vége</span></label> <label class="label"><span class="label-text">Ismétlődés vége</span></label>
<input type="date" <input type="date" formControlName="endDate" class="input input-bordered w-full" />
formControlName="endDate"
class="input input-bordered w-full" />
</div> </div>
</div> </div>
} }
<div class="card-actions justify-end mt-6"> <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"> <button type="submit" class="btn btn-primary" [disabled]="form.invalid">
{{ isEditMode ? 'Mentés' : 'Létrezhozás' }} {{ isEditMode ? 'Mentés' : 'Létrezhozás' }}
</button> </button>
<a class="btn btn-primary" (click)="doReady()">Mégse</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -44,4 +44,19 @@ export class SvgIcons {
</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

@@ -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

@@ -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> = {
@@ -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)