add calendarview event creation
This commit is contained in:
parent
008b644bb1
commit
6b975dadac
11
admin/package-lock.json
generated
11
admin/package-lock.json
generated
@ -22,6 +22,7 @@
|
|||||||
"@fullcalendar/timegrid": "^6.1.19",
|
"@fullcalendar/timegrid": "^6.1.19",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"daisyui": "^5.4.5",
|
"daisyui": "^5.4.5",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
@ -4908,6 +4909,16 @@
|
|||||||
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/date-format": {
|
"node_modules/date-format": {
|
||||||
"version": "4.0.14",
|
"version": "4.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
"@fullcalendar/timegrid": "^6.1.19",
|
"@fullcalendar/timegrid": "^6.1.19",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"daisyui": "^5.4.5",
|
"daisyui": "^5.4.5",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
<div class="card bg-base-100 w-96 shadow-sm" [class]="cardClass()">
|
||||||
|
@if (imageSrc()) {
|
||||||
|
<figure class="px-10 pt-10">
|
||||||
|
<img
|
||||||
|
[src]="imageSrc()"
|
||||||
|
[alt]="imageAlt()"
|
||||||
|
class="rounded-xl"
|
||||||
|
[class]="imageClass()"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
}
|
||||||
|
<div class="card-body items-center text-center">
|
||||||
|
@if (cardTitle()) {
|
||||||
|
<h2 class="card-title">{{ cardTitle() }}</h2>
|
||||||
|
}
|
||||||
|
@if (cardText()) {
|
||||||
|
<p>A card component has a figure, a body part, and inside body there are title and actions parts</p>
|
||||||
|
}
|
||||||
|
<ng-content></ng-content>
|
||||||
|
@if (cardActionText()) {
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn btn-primary">{{ cardActionText() }}</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { Component, input, output, signal } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'rs-daisy-card-with-centered-content-and-paddings',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './card-with-centered-content-and-paddings.html',
|
||||||
|
styleUrl: './card-with-centered-content-and-paddings.css',
|
||||||
|
})
|
||||||
|
export class CardWithCenteredContentAndPaddings {
|
||||||
|
imageSrc = input<string | null>(null);
|
||||||
|
imageAlt = input<string | null>(null);
|
||||||
|
imageClass = input<string | null>(null);
|
||||||
|
cardTitle = input<string | null>(null);
|
||||||
|
cardText = input<string | null>(null);
|
||||||
|
cardActionText = input<string | null>(null);
|
||||||
|
cardActionClick = output<void>();
|
||||||
|
cardClass = signal<string | null>(null);
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
{{ isOpen() }}
|
||||||
|
<dialog #modal class="modal" [class]="dialogStyleClass()">
|
||||||
|
<div class="modal-box" [class]="modalBoxStyleClass()">
|
||||||
|
<button (click)="closeClicked()" class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||||
|
@if (headerText()) {
|
||||||
|
<h3 class="text-lg font-bold">{{ headerText() }}</h3>
|
||||||
|
}
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
@if (backdrop()) {
|
||||||
|
<form (click)="closeClicked()" class="modal-backdrop">
|
||||||
|
<button>close</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</dialog>
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Modal } from './modal';
|
||||||
|
|
||||||
|
describe('Modal', () => {
|
||||||
|
let component: Modal;
|
||||||
|
let fixture: ComponentFixture<Modal>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Modal]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Modal);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
Component,
|
||||||
|
effect,
|
||||||
|
ElementRef,
|
||||||
|
input,
|
||||||
|
model, OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
output,
|
||||||
|
signal,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'rs-daisy-modal',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './modal.html',
|
||||||
|
styleUrl: './modal.css',
|
||||||
|
})
|
||||||
|
export class Modal implements OnInit , AfterViewInit, OnDestroy{
|
||||||
|
dialogStyleClass = input<string>();
|
||||||
|
backdrop = input<boolean>(false);
|
||||||
|
modalBoxStyleClass = input<string>();
|
||||||
|
closeButton = input<boolean>(true);
|
||||||
|
headerText = input<string>();
|
||||||
|
isOpen = model<boolean>(false);
|
||||||
|
|
||||||
|
onClose = output<boolean>();
|
||||||
|
closeClick = output<boolean>();
|
||||||
|
|
||||||
|
@ViewChild('modal') modalRef!: ElementRef<HTMLDialogElement>;
|
||||||
|
|
||||||
|
private initialized = signal<boolean>(false);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
if ( this.isOpen()){
|
||||||
|
this.doOpen();
|
||||||
|
}else{
|
||||||
|
this.doClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
/* if (open()){
|
||||||
|
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
// console.info("dialog",this.dialog)
|
||||||
|
const modal = this.modalRef.nativeElement;
|
||||||
|
modal.addEventListener('close', () => this.onClose.emit(true) );
|
||||||
|
this.initialized.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitClose(){
|
||||||
|
console.info("emit close", this.onClose);
|
||||||
|
this.onClose?.emit(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
doOpen() {
|
||||||
|
if ( !this.initialized()){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modal = this.modalRef.nativeElement;
|
||||||
|
if ( !modal.open ){
|
||||||
|
modal.showModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doClose() {
|
||||||
|
if ( !this.initialized()){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modal = this.modalRef.nativeElement;
|
||||||
|
if ( modal.open ){
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ngOnDestroy() {
|
||||||
|
console.info("destroy");
|
||||||
|
// const modal = this.modalRef?.nativeElement;
|
||||||
|
// if ( modal ){
|
||||||
|
// modal.removeEventListener('close', this.emitClose);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
closeClicked() {
|
||||||
|
console.info("close clicked");
|
||||||
|
this.closeClick.emit(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,5 +6,7 @@ export * from './lib/ng-daisyui';
|
|||||||
export * from './lib/components/button/button';
|
export * from './lib/components/button/button';
|
||||||
export * from './lib/components/footer/footer';
|
export * from './lib/components/footer/footer';
|
||||||
export * from './lib/components/breadcrumbs/breadcrumbs';
|
export * from './lib/components/breadcrumbs/breadcrumbs';
|
||||||
|
export * from './lib/components/modal/modal';
|
||||||
|
export * from './lib/components/card/card-with-centered-content-and-paddings/card-with-centered-content-and-paddings';
|
||||||
export * from './lib/daisy.types';
|
export * from './lib/daisy.types';
|
||||||
export * from './lib/layout/';
|
export * from './lib/layout/';
|
||||||
|
|||||||
@ -6,3 +6,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<full-calendar #calendar [options]="calendarOptions"></full-calendar>
|
<full-calendar #calendar [options]="calendarOptions"></full-calendar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<rs-daisy-modal [isOpen]="isOpen()" (closeClick)="closeDialog()" >
|
||||||
|
<app-create-event-form (ready)="closeDialog()"></app-create-event-form>
|
||||||
|
</rs-daisy-modal>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Component, ElementRef, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, effect, ElementRef, inject, OnInit, signal, ViewChild } from '@angular/core';
|
||||||
import { FullCalendarComponent, FullCalendarModule } from '@fullcalendar/angular';
|
import { FullCalendarComponent, FullCalendarModule } from '@fullcalendar/angular';
|
||||||
import { CalendarOptions } from '@fullcalendar/core';
|
import { CalendarOptions } from '@fullcalendar/core';
|
||||||
|
|
||||||
@ -6,19 +6,30 @@ import dayGridPlugin from '@fullcalendar/daygrid';
|
|||||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||||
import listPlugin from '@fullcalendar/list';
|
import listPlugin from '@fullcalendar/list';
|
||||||
import interactionPlugin from '@fullcalendar/interaction';
|
import interactionPlugin from '@fullcalendar/interaction';
|
||||||
import { appConfig } from '../../../../app.config';
|
import { Modal } from '@rschneider/ng-daisyui';
|
||||||
|
import { CreateEventForm } from '../create-event-form/create-event-form';
|
||||||
|
import { CalendarService } from '../../services/calendar.service';
|
||||||
|
import { addDays, subDays } from 'date-fns';
|
||||||
|
import { EventsInRangeDTO } from '../../models/events-in-range-dto.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-calendar-view',
|
selector: 'app-calendar-view',
|
||||||
imports: [FullCalendarModule],
|
imports: [FullCalendarModule, Modal, CreateEventForm],
|
||||||
templateUrl: './calendar-view.html',
|
templateUrl: './calendar-view.html',
|
||||||
styleUrl: './calendar-view.css',
|
styleUrl: './calendar-view.css',
|
||||||
})
|
})
|
||||||
export class CalendarView {
|
export class CalendarView implements OnInit, AfterViewInit {
|
||||||
|
|
||||||
@ViewChild('startHour') startHour!: ElementRef;
|
@ViewChild('startHour') startHour!: ElementRef;
|
||||||
@ViewChild('calendar') calendarComponent: FullCalendarComponent | undefined;
|
@ViewChild('calendar') calendarComponent: FullCalendarComponent | undefined;
|
||||||
|
|
||||||
|
calendarService = inject(CalendarService);
|
||||||
|
|
||||||
|
workflow = signal<string>("")
|
||||||
|
isOpen = signal<boolean>(false);
|
||||||
|
|
||||||
|
events = signal<EventsInRangeDTO[]>([])
|
||||||
|
|
||||||
calendarOptions: CalendarOptions;
|
calendarOptions: CalendarOptions;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -27,6 +38,7 @@ export class CalendarView {
|
|||||||
const end = new Date();
|
const end = new Date();
|
||||||
end.setHours(11,0,0)
|
end.setHours(11,0,0)
|
||||||
|
|
||||||
|
|
||||||
this.calendarOptions = {
|
this.calendarOptions = {
|
||||||
plugins: [dayGridPlugin, timeGridPlugin, listPlugin,interactionPlugin],
|
plugins: [dayGridPlugin, timeGridPlugin, listPlugin,interactionPlugin],
|
||||||
initialView: 'dayGridMonth',
|
initialView: 'dayGridMonth',
|
||||||
@ -41,7 +53,6 @@ export class CalendarView {
|
|||||||
events: [
|
events: [
|
||||||
|
|
||||||
|
|
||||||
{ title: 'Meeting1 until'+end.toString(), start, end },
|
|
||||||
],
|
],
|
||||||
|
|
||||||
eventClick: function(info) {
|
eventClick: function(info) {
|
||||||
@ -51,21 +62,54 @@ export class CalendarView {
|
|||||||
info.el.style.borderColor = 'red';
|
info.el.style.borderColor = 'red';
|
||||||
},
|
},
|
||||||
|
|
||||||
dateClick: function(info) {
|
dateClick: (info) => {
|
||||||
console.info('Date click on: ' , info);
|
console.info("setting day workflow");
|
||||||
const calendarApi = info.view.calendar;
|
this.workflow.set("day");
|
||||||
const start = new Date(info.date.getTime())
|
this.isOpen.set(true);
|
||||||
start.setHours(2,0,0)
|
// console.info('Date click on: ' , info);
|
||||||
const end = new Date(info.date.getTime())
|
// const calendarApi = info.view.calendar;
|
||||||
end.setHours(3,0,0)
|
// const start = new Date(info.date.getTime())
|
||||||
calendarApi.addEvent({
|
// start.setHours(2,0,0)
|
||||||
title: 'New Event',
|
// const end = new Date(info.date.getTime())
|
||||||
start,
|
// end.setHours(3,0,0)
|
||||||
end,
|
// calendarApi.addEvent({
|
||||||
});
|
// title: 'New Event',
|
||||||
|
// start,
|
||||||
|
// end,
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
// this.calendarOptions.events = this.events
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
|
||||||
|
// this.calendarComponent?.getApi().
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const start = subDays(new Date(),14)
|
||||||
|
const end = addDays(new Date(),14);
|
||||||
|
this.calendarService.getEventsInRange({
|
||||||
|
startTime: start,
|
||||||
|
endTime: end
|
||||||
|
}).subscribe(
|
||||||
|
{
|
||||||
|
'next': (events) => {
|
||||||
|
console.info('events',events)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected addEvent($event: PointerEvent) {
|
protected addEvent($event: PointerEvent) {
|
||||||
@ -82,4 +126,8 @@ export class CalendarView {
|
|||||||
start
|
start
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeDialog() {
|
||||||
|
this.isOpen.set(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,51 @@
|
|||||||
|
<!-- Generated by the CLI -->
|
||||||
|
<div class="p-4 md:p-8">
|
||||||
|
<div class="card bg-base-100 shadow-xl max-w-2xl mx-auto">
|
||||||
|
<div class="card-body">
|
||||||
|
<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">
|
||||||
|
|
||||||
|
<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="is_recurring" class="checkbox" />
|
||||||
|
</label></div>
|
||||||
|
|
||||||
|
<div class="card-actions justify-end mt-6">
|
||||||
|
<a routerLink="/events" class="btn btn-ghost">Mégse</a>
|
||||||
|
<button type="submit" class="btn btn-primary" [disabled]="form.invalid">
|
||||||
|
{{ isEditMode ? 'Mentés' : 'Létrezhozás' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,146 @@
|
|||||||
|
import { Component, input, OnInit, output, signal } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { EventService } from '../../../events/services/event.service';
|
||||||
|
import { mergeMap, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { Event } from '../../../events/models/event.model';
|
||||||
|
import { EventType } from '../../../event-type/models/event-type.model';
|
||||||
|
import { EventTypeService } from '../../../event-type/services/event-type.service';
|
||||||
|
import { CalendarService } from '../../services/calendar.service';
|
||||||
|
import { EventFormDTO } from '../../models/event-form-dto.model';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-create-event-form',
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './create-event-form.html',
|
||||||
|
styleUrl: './create-event-form.css',
|
||||||
|
})
|
||||||
|
export class CreateEventForm implements OnInit {
|
||||||
|
form: FormGroup;
|
||||||
|
isEditMode = false;
|
||||||
|
ready = output<void>();
|
||||||
|
id= input<number | null>() ;
|
||||||
|
|
||||||
|
eventTypes = signal<EventType[]>([])
|
||||||
|
|
||||||
|
private numericFields = ["event_type_id"];
|
||||||
|
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private eventService: EventService,
|
||||||
|
private calendarService: CalendarService,
|
||||||
|
private eventTypeService: EventTypeService
|
||||||
|
) {
|
||||||
|
this.form = this.fb.group({
|
||||||
|
eventTypeId: [null,Validators.required],
|
||||||
|
title: [null,Validators.required],
|
||||||
|
description: [null],
|
||||||
|
startTime: [null,Validators.required],
|
||||||
|
endTime: [null,Validators.required],
|
||||||
|
is_recurring: [null],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
of(this.id()).pipe(
|
||||||
|
tap(id => {
|
||||||
|
if (id) {
|
||||||
|
this.isEditMode = true;
|
||||||
|
}else{
|
||||||
|
|
||||||
|
const start = new Date();
|
||||||
|
const end = new Date();
|
||||||
|
start.setMinutes(0,0);
|
||||||
|
end.setHours(start.getHours()+1,0,0);
|
||||||
|
start.setMinutes(start.getMinutes() - start.getTimezoneOffset());
|
||||||
|
const startString = start.toISOString().slice(0,16);
|
||||||
|
|
||||||
|
end.setMinutes(end.getMinutes() - end.getTimezoneOffset());
|
||||||
|
const endTime = end.toISOString().slice(0,16);
|
||||||
|
|
||||||
|
console.info("Date start",start.toLocaleString("hu-HU", {timeStyle: 'short', dateStyle: 'short'}));
|
||||||
|
this.form.patchValue({
|
||||||
|
// start_time: new Date().getTime().toString(),
|
||||||
|
startTime: startString,
|
||||||
|
endTime: endTime
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
mergeMap(() => {
|
||||||
|
return this.eventTypeService.find({});
|
||||||
|
}
|
||||||
|
),
|
||||||
|
tap(eventTypes => {
|
||||||
|
this.eventTypes.set(eventTypes.data);
|
||||||
|
}),
|
||||||
|
switchMap(() => {
|
||||||
|
if (this.isEditMode && this.id()) {
|
||||||
|
return this.eventService.findOne(this.id()!);
|
||||||
|
}
|
||||||
|
return of(null);
|
||||||
|
}),
|
||||||
|
).subscribe(event => {
|
||||||
|
if (event) {
|
||||||
|
this.form.patchValue(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
if (this.form.invalid) {
|
||||||
|
this.form.markAllAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: EventFormDTO|any = { ...this.form.value };
|
||||||
|
|
||||||
|
for (const field of this.numericFields) {
|
||||||
|
if (payload[field] != null && payload[field] !== '') {
|
||||||
|
payload[field] = parseFloat(payload[field]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let action$: Observable<Event>;
|
||||||
|
|
||||||
|
if (this.isEditMode && this.id()) {
|
||||||
|
console.info("rong branch")
|
||||||
|
// action$ = this.calendarService.update(this.id()!, payload);
|
||||||
|
action$ = of(payload);
|
||||||
|
} else {
|
||||||
|
action$ = this.calendarService.create(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
action$.subscribe({
|
||||||
|
next: () => this.router.navigate(['/events']),
|
||||||
|
error: (err) => console.error('Failed to save event', err)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get eventType(){
|
||||||
|
return this.form.get('eventTypeId');
|
||||||
|
}
|
||||||
|
|
||||||
|
get title(){
|
||||||
|
return this.form.get('title');
|
||||||
|
}
|
||||||
|
get description(){
|
||||||
|
return this.form.get('description');
|
||||||
|
}
|
||||||
|
get startTime(){
|
||||||
|
return this.form.get('startTime');
|
||||||
|
}
|
||||||
|
get endTime(){
|
||||||
|
return this.form.get('endTime');
|
||||||
|
}
|
||||||
|
|
||||||
|
doReady(){
|
||||||
|
this.ready.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
// dvbooking-cli/src/templates/angular/model.ts.tpl
|
||||||
|
|
||||||
|
// Generated by the CLI
|
||||||
|
export interface EventFormDTO {
|
||||||
|
id?: number;
|
||||||
|
event_type_id: number;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
start_time?: Date;
|
||||||
|
end_time?: Date;
|
||||||
|
is_recurring: boolean;
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { Event } from '../../events/models/event.model';
|
||||||
|
export interface EventsInRangeDTO {
|
||||||
|
startTime?: Date;
|
||||||
|
endTime?: Date;
|
||||||
|
}
|
||||||
|
export type BookingUserDto = {
|
||||||
|
id: number;
|
||||||
|
name: string; // Assuming user has a 'name' property
|
||||||
|
email: string; // Assuming user has a 'name' property
|
||||||
|
};
|
||||||
|
|
||||||
|
// This represents a booking with nested user info
|
||||||
|
export type BookingWithUserDto = {
|
||||||
|
user: BookingUserDto | null;
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The final shape of a calendar event occurrence
|
||||||
|
export type CalendarEventDto = Omit<Event, 'bookings'> & {
|
||||||
|
isModified?: boolean;
|
||||||
|
eventBookings: BookingWithUserDto[];
|
||||||
|
};
|
||||||
39
admin/src/app/features/calendar/services/calendar.service.ts
Normal file
39
admin/src/app/features/calendar/services/calendar.service.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ConfigurationService } from '../../../services/configuration.service';
|
||||||
|
import { EventFormDTO } from '../models/event-form-dto.model';
|
||||||
|
import { Event } from '../../events/models/event.model';
|
||||||
|
import { EventsInRangeDTO } from '../models/events-in-range-dto.model';
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class CalendarService {
|
||||||
|
private readonly apiUrl: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private configService: ConfigurationService
|
||||||
|
) {
|
||||||
|
this.apiUrl = `${this.configService.getApiUrl()}/calendar`;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* get events in range
|
||||||
|
*/
|
||||||
|
public getEventsInRange(eventsInRangeDto: EventsInRangeDTO): Observable<EventsInRangeDTO[]> {
|
||||||
|
const params = new HttpParams()
|
||||||
|
.set('startDate', eventsInRangeDto.startTime!.toISOString())
|
||||||
|
.set('endDate', eventsInRangeDto.endTime!.toISOString());
|
||||||
|
return this.http.get<EventsInRangeDTO[]>(this.apiUrl+'', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new record.
|
||||||
|
*/
|
||||||
|
public create(data: EventFormDTO): Observable<Event> {
|
||||||
|
return this.http.post<Event>(this.apiUrl+'/events', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,5 +1,3 @@
|
|||||||
<!-- dvbooking-cli/src/templates/angular/form.component.html.tpl -->
|
|
||||||
|
|
||||||
<!-- Generated by the CLI -->
|
<!-- Generated by the CLI -->
|
||||||
<div class="p-4 md:p-8">
|
<div class="p-4 md:p-8">
|
||||||
<div class="card bg-base-100 shadow-xl max-w-2xl mx-auto">
|
<div class="card bg-base-100 shadow-xl max-w-2xl mx-auto">
|
||||||
|
|||||||
@ -6,10 +6,17 @@ import { RecurrenceRule } from '../entity/recurrence-rule.entity';
|
|||||||
import { EventException } from '../entity/event-exception.entity';
|
import { EventException } from '../entity/event-exception.entity';
|
||||||
import { Event } from '../entity/event.entity';
|
import { Event } from '../entity/event.entity';
|
||||||
import { Booking } from '../entity/booking.entity';
|
import { Booking } from '../entity/booking.entity';
|
||||||
|
import { EventType } from '../entity/event-type.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([RecurrenceRule, EventException, Event, Booking]),
|
TypeOrmModule.forFeature([
|
||||||
|
RecurrenceRule,
|
||||||
|
EventException,
|
||||||
|
Event,
|
||||||
|
Booking,
|
||||||
|
EventType,
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
controllers: [CalendarController],
|
controllers: [CalendarController],
|
||||||
providers: [CalendarService],
|
providers: [CalendarService],
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { CreateExceptionDto } from './dto/create-exception.dto';
|
|||||||
import { Booking } from '../entity/booking.entity';
|
import { Booking } from '../entity/booking.entity';
|
||||||
import { CancelBookingDto } from './dto/cancel-booking.dto';
|
import { CancelBookingDto } from './dto/cancel-booking.dto';
|
||||||
import { CreateBookingDto } from './dto/create-booking.dto';
|
import { CreateBookingDto } from './dto/create-booking.dto';
|
||||||
|
import { EventType } from '../entity/event-type.entity';
|
||||||
|
|
||||||
// --- Type-Safe Maps ---
|
// --- Type-Safe Maps ---
|
||||||
const frequencyMap: Record<string, RRule.Frequency> = {
|
const frequencyMap: Record<string, RRule.Frequency> = {
|
||||||
@ -60,6 +61,8 @@ export class CalendarService {
|
|||||||
private readonly dataSource: DataSource,
|
private readonly dataSource: DataSource,
|
||||||
@InjectRepository(Event)
|
@InjectRepository(Event)
|
||||||
private readonly eventRepository: Repository<Event>,
|
private readonly eventRepository: Repository<Event>,
|
||||||
|
@InjectRepository(EventType)
|
||||||
|
private readonly eventTypeRepository: Repository<EventType>,
|
||||||
@InjectRepository(RecurrenceRule)
|
@InjectRepository(RecurrenceRule)
|
||||||
private readonly recurrenceRuleRepository: Repository<RecurrenceRule>,
|
private readonly recurrenceRuleRepository: Repository<RecurrenceRule>,
|
||||||
@InjectRepository(EventException)
|
@InjectRepository(EventException)
|
||||||
@ -196,9 +199,37 @@ export class CalendarService {
|
|||||||
// --- Other service methods (createEvent, etc.) remain unchanged ---
|
// --- Other service methods (createEvent, etc.) remain unchanged ---
|
||||||
|
|
||||||
async createEvent(createEventDto: CreateEventDto): Promise<Event> {
|
async createEvent(createEventDto: CreateEventDto): Promise<Event> {
|
||||||
|
console.info('createEvent', createEventDto);
|
||||||
const { recurrenceRule, ...eventData } = createEventDto;
|
const { recurrenceRule, ...eventData } = createEventDto;
|
||||||
|
|
||||||
const event = this.eventRepository.create(eventData);
|
const newEvent: Omit<
|
||||||
|
Event,
|
||||||
|
'id' | 'bookings' | 'exceptions' | 'recurrenceRule'
|
||||||
|
> = {
|
||||||
|
...eventData,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
timezone: 'Europe/Budapest',
|
||||||
|
eventType: undefined,
|
||||||
|
isRecurring: recurrenceRule ? true : false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// check if event type exists
|
||||||
|
if (eventData.eventTypeId) {
|
||||||
|
console.info('eventTypeId found');
|
||||||
|
const eventType = await this.eventTypeRepository.findOneBy({
|
||||||
|
id: eventData.eventTypeId,
|
||||||
|
});
|
||||||
|
if (!eventType) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
{},
|
||||||
|
'Event type not found ' + eventData.eventTypeId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
newEvent.eventType = eventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = this.eventRepository.create(newEvent);
|
||||||
const savedEvent = await this.eventRepository.save(event);
|
const savedEvent = await this.eventRepository.save(event);
|
||||||
|
|
||||||
if (event.isRecurring && recurrenceRule) {
|
if (event.isRecurring && recurrenceRule) {
|
||||||
|
|||||||
@ -55,9 +55,9 @@ export class CreateEventDto {
|
|||||||
@Type(() => Date)
|
@Type(() => Date)
|
||||||
endTime: Date;
|
endTime: Date;
|
||||||
|
|
||||||
@IsNotEmpty()
|
// @IsNotEmpty()
|
||||||
@IsString()
|
// @IsString()
|
||||||
timezone: string; // e.g., 'Europe/Berlin'
|
// timezone: string; // e.g., 'Europe/Berlin'
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export class Event {
|
|||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
@Column({ name: 'description', type: 'text', nullable: true })
|
@Column({ name: 'description', type: 'text', nullable: true })
|
||||||
description: string;
|
description?: string;
|
||||||
|
|
||||||
@Column({ name: 'start_time', type: 'timestamptz' })
|
@Column({ name: 'start_time', type: 'timestamptz' })
|
||||||
startTime: Date;
|
startTime: Date;
|
||||||
@ -45,15 +45,15 @@ export class Event {
|
|||||||
|
|
||||||
// --- Relationships ---
|
// --- Relationships ---
|
||||||
|
|
||||||
@Column({ name: 'event_type_id', type: 'bigint', nullable: true })
|
// @Column({ name: 'event_type_id', type: 'bigint', nullable: true })
|
||||||
eventTypeId: number;
|
// eventTypeId: number;
|
||||||
|
|
||||||
@ManyToOne(() => EventType, (eventType) => eventType.events, {
|
@ManyToOne(() => EventType, (eventType) => eventType.events, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
onDelete: 'SET NULL', // As requested for optional relationship
|
onDelete: 'SET NULL', // As requested for optional relationship
|
||||||
})
|
})
|
||||||
@JoinColumn({ name: 'event_type_id' })
|
@JoinColumn({ name: 'event_type_id' })
|
||||||
eventType: EventType;
|
eventType?: EventType;
|
||||||
|
|
||||||
@OneToOne(() => RecurrenceRule, (rule) => rule.event, {
|
@OneToOne(() => RecurrenceRule, (rule) => rule.event, {
|
||||||
cascade: true, // Automatically save/update recurrence rule when event is saved
|
cascade: true, // Automatically save/update recurrence rule when event is saved
|
||||||
|
|||||||
@ -26,6 +26,7 @@ async function bootstrap() {
|
|||||||
transformOptions: {
|
transformOptions: {
|
||||||
enableImplicitConversion: true, // Allows class-transformer to convert string primitives to their target types
|
enableImplicitConversion: true, // Allows class-transformer to convert string primitives to their target types
|
||||||
},
|
},
|
||||||
|
enableDebugMessages: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user