diff --git a/bot/src/bot_core/configuration/feature_flags_enum.py b/bot/src/bot_core/configuration/feature_flags_enum.py index 6d051b0c..8506af95 100644 --- a/bot/src/bot_core/configuration/feature_flags_enum.py +++ b/bot/src/bot_core/configuration/feature_flags_enum.py @@ -27,3 +27,4 @@ class FeatureFlagsEnum(Enum): short_role_name = "ShortRoleName" technician_full_access = "TechnicianFullAccess" steam_special_offers = "SteamSpecialOffers" + scheduled_events = "ScheduledEvents" diff --git a/bot/src/bot_core/configuration/feature_flags_settings.py b/bot/src/bot_core/configuration/feature_flags_settings.py index 0ed95e71..3b994b17 100644 --- a/bot/src/bot_core/configuration/feature_flags_settings.py +++ b/bot/src/bot_core/configuration/feature_flags_settings.py @@ -29,6 +29,7 @@ class FeatureFlagsSettings(ConfigurationModelABC): FeatureFlagsEnum.short_role_name.value: False, # 28.09.2023 #378 FeatureFlagsEnum.technician_full_access.value: False, # 03.10.2023 #393 FeatureFlagsEnum.steam_special_offers.value: False, # 11.10.2023 #188 + FeatureFlagsEnum.scheduled_events.value: False, # 14.11.2023 #410 } def __init__(self, **kwargs: dict): diff --git a/bot/src/bot_data/data_module.py b/bot/src/bot_data/data_module.py index cec9cb37..5d20e874 100644 --- a/bot/src/bot_data/data_module.py +++ b/bot/src/bot_data/data_module.py @@ -14,6 +14,7 @@ from bot_data.abc.data_seeder_abc import DataSeederABC from bot_data.abc.game_server_repository_abc import GameServerRepositoryABC from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC from bot_data.abc.level_repository_abc import LevelRepositoryABC +from bot_data.abc.scheduled_event_repository_abc import ScheduledEventRepositoryABC from bot_data.abc.server_config_repository_abc import ServerConfigRepositoryABC from bot_data.abc.server_repository_abc import ServerRepositoryABC from bot_data.abc.short_role_name_repository_abc import ShortRoleNameRepositoryABC @@ -45,6 +46,7 @@ from bot_data.service.client_repository_service import ClientRepositoryService from bot_data.service.game_server_repository_service import GameServerRepositoryService from bot_data.service.known_user_repository_service import KnownUserRepositoryService from bot_data.service.level_repository_service import LevelRepositoryService +from bot_data.service.scheduled_event_repository_service import ScheduledEventRepositoryService from bot_data.service.seeder_service import SeederService from bot_data.service.server_config_repository_service import ( ServerConfigRepositoryService, @@ -115,6 +117,7 @@ class DataModule(ModuleABC): services.add_transient(ServerConfigRepositoryABC, ServerConfigRepositoryService) services.add_transient(ShortRoleNameRepositoryABC, ShortRoleNameRepositoryService) services.add_transient(SteamSpecialOfferRepositoryABC, SteamSpecialOfferRepositoryService) + services.add_transient(ScheduledEventRepositoryABC, ScheduledEventRepositoryService) services.add_transient(SeederService) services.add_transient(DataSeederABC, TechnicianConfigSeeder) diff --git a/web/src/app/models/data/scheduled_events.model.ts b/web/src/app/models/data/scheduled_events.model.ts new file mode 100644 index 00000000..c3e8beea --- /dev/null +++ b/web/src/app/models/data/scheduled_events.model.ts @@ -0,0 +1,34 @@ +import { DataWithHistory } from "./data.model"; +import { Server, ServerFilter } from "./server.model"; + +export enum EventType { + stageInstance = 1, + voice = 2, + external = 3, +} + +export interface ScheduledEvent extends DataWithHistory { + id?: number; + interval?: string; + name?: string; + description?: string; + channelId?: string; + startTime?: string; + endTime?: string; + entityType?: EventType; + location?: string; + server?: Server; +} + +export interface ScheduledEventFilter { + id?: number; + interval?: string; + name?: string; + description?: string; + channelId?: string; + startTime?: string; + endTime?: string; + entityType?: number; + location?: string; + server?: ServerFilter; +} diff --git a/web/src/app/models/graphql/mutations.model.ts b/web/src/app/models/graphql/mutations.model.ts index 4f52fa01..27fd48c1 100644 --- a/web/src/app/models/graphql/mutations.model.ts +++ b/web/src/app/models/graphql/mutations.model.ts @@ -173,6 +173,70 @@ export class Mutations { } `; + static createScheduledEvent = ` + mutation createScheduledEvent($interval: String,$name: String,$description: String,$channelId: String,$startTime: String, $endTime: String,$entityType: Int,$location: String, $serverId: ID) { + scheduledEvent { + createScheduledEvent(input: { + interval: $interval, + name: $name, + description: $description, + channelId: $channelId, + startTime: $startTime, + endTime: $endTime, + entityType: $entityType, + location: $location, + serverId: $serverId} + ) { + id + name + description + attribute + operator + value + server { + id + } + } + } + } + `; + + static updateScheduledEvent = ` + mutation updateScheduledEvent($interval: String,$name: String,$description: String,$channelId: String,$startTime: String, $endTime: String,$entityType: Int,$location: String, $serverId: ID) { + scheduledEvent { + updateScheduledEvent(input: { + interval: $interval, + name: $name, + description: $description, + channelId: $channelId, + startTime: $startTime, + endTime: $endTime, + entityType: $entityType, + location: $location, + serverId: $serverId} + ) { + id + name + description + attribute + operator + value + } + } + } + `; + + static deleteScheduledEvent = ` + mutation deleteScheduledEvent($id: ID) { + scheduledEvent { + deleteScheduledEvent(id: $id) { + id + name + } + } + } + `; + static createShortRoleName = ` mutation createShortRoleName($shortName: String, $roleId: String, $position: String, $serverId: ID) { shortRoleName { diff --git a/web/src/app/models/graphql/queries.model.ts b/web/src/app/models/graphql/queries.model.ts index e44e3aab..1365d76e 100644 --- a/web/src/app/models/graphql/queries.model.ts +++ b/web/src/app/models/graphql/queries.model.ts @@ -229,6 +229,58 @@ export class Queries { } `; + static scheduledEventQuery = ` + query ScheduledEventList($serverId: ID, $filter: ScheduledEventFilter, $page: Page, $sort: Sort) { + servers(filter: {id: $serverId}) { + scheduledEventCount + scheduledEvents(filter: $filter, page: $page, sort: $sort) { + id + interval + name + description + channelId + startTime + endTime + entityType + location + server { + id + name + } + createdAt + modifiedAt + } + } + } + `; + + static scheduledEventWithHistoryQuery = ` + query ScheduledEventHistory($serverId: ID, $id: ID) { + servers(filter: {id: $serverId}) { + scheduledEventCount + scheduledEvents(filter: {id: $id}) { + id + + history { + id + interval + name + description + channelId + startTime + endTime + entityType + location + server + deleted + dateFrom + dateTo + } + } + } + } + `; + static shortRoleNamePositionsQuery = ` query { diff --git a/web/src/app/models/graphql/query.model.ts b/web/src/app/models/graphql/query.model.ts index f98f9a4a..b7da6553 100644 --- a/web/src/app/models/graphql/query.model.ts +++ b/web/src/app/models/graphql/query.model.ts @@ -9,6 +9,7 @@ import { ServerConfig } from "../config/server-config.model"; import { ShortRoleName } from "../data/short_role_name.model"; import { FeatureFlag } from "../config/feature-flags.model"; import { UserWarning } from "../data/user_warning.model"; +import { ScheduledEvent } from "../data/scheduled_events.model"; export interface Query { serverCount: number; @@ -57,6 +58,11 @@ export interface AchievementListQuery { achievements: Achievement[]; } +export interface ScheduledEventListQuery { + scheduledEventCount: number; + scheduledEvents: ScheduledEvent[]; +} + export interface AutoRoleQuery { autoRoleCount: number; autoRoles: AutoRole[]; diff --git a/web/src/app/models/graphql/result.model.ts b/web/src/app/models/graphql/result.model.ts index 411d6f1d..2b9844a7 100644 --- a/web/src/app/models/graphql/result.model.ts +++ b/web/src/app/models/graphql/result.model.ts @@ -7,6 +7,7 @@ import { TechnicianConfig } from "../config/technician-config.model"; import { ServerConfig } from "../config/server-config.model"; import { ShortRoleName } from "../data/short_role_name.model"; import { UserWarning } from "../data/user_warning.model"; +import { ScheduledEvent } from "../data/scheduled_events.model"; export interface GraphQLResult { data: { @@ -71,6 +72,14 @@ export interface AchievementMutationResult { }; } +export interface ScheduledEventMutationResult { + scheduledEvent: { + createScheduledEvent?: ScheduledEvent + updateScheduledEvent?: ScheduledEvent + deleteScheduledEvent?: ScheduledEvent + }; +} + export interface ShortRoleNameMutationResult { shortRoleName: { createShortRoleName?: ShortRoleName diff --git a/web/src/app/modules/shared/shared.module.ts b/web/src/app/modules/shared/shared.module.ts index b3ca8e60..2a7bfc45 100644 --- a/web/src/app/modules/shared/shared.module.ts +++ b/web/src/app/modules/shared/shared.module.ts @@ -13,7 +13,7 @@ import { InputTextModule } from "primeng/inputtext"; import { MenuModule } from "primeng/menu"; import { PasswordModule } from "primeng/password"; import { ProgressSpinnerModule } from "primeng/progressspinner"; -import { SortableColumn, TableModule } from "primeng/table"; +import { TableModule } from "primeng/table"; import { ToastModule } from "primeng/toast"; import { AuthRolePipe } from "./pipes/auth-role.pipe"; import { IpAddressPipe } from "./pipes/ip-address.pipe"; @@ -24,18 +24,20 @@ import { InputNumberModule } from "primeng/inputnumber"; import { ImageModule } from "primeng/image"; import { SidebarModule } from "primeng/sidebar"; import { HistoryBtnComponent } from "./components/history-btn/history-btn.component"; -import { DataViewModule, DataViewLayoutOptions } from "primeng/dataview"; +import { DataViewLayoutOptions, DataViewModule } from "primeng/dataview"; import { ConfigListComponent } from "./components/config-list/config-list.component"; import { MultiSelectModule } from "primeng/multiselect"; -import { HideableColumnComponent } from './components/hideable-column/hideable-column.component'; -import { HideableHeaderComponent } from './components/hideable-header/hideable-header.component'; -import { MultiSelectColumnsComponent } from './base/multi-select-columns/multi-select-columns.component'; -import { FeatureFlagListComponent } from './components/feature-flag-list/feature-flag-list.component'; +import { HideableColumnComponent } from "./components/hideable-column/hideable-column.component"; +import { HideableHeaderComponent } from "./components/hideable-header/hideable-header.component"; +import { MultiSelectColumnsComponent } from "./base/multi-select-columns/multi-select-columns.component"; +import { FeatureFlagListComponent } from "./components/feature-flag-list/feature-flag-list.component"; import { InputSwitchModule } from "primeng/inputswitch"; import { CalendarModule } from "primeng/calendar"; -import { DataImportAndExportComponent } from './components/data-import-and-export/data-import-and-export.component'; +import { DataImportAndExportComponent } from "./components/data-import-and-export/data-import-and-export.component"; import { FileUploadModule } from "primeng/fileupload"; import { SelectButtonModule } from "primeng/selectbutton"; +import { TabViewModule } from "primeng/tabview"; +import { RadioButtonModule } from "primeng/radiobutton"; const PrimeNGModules = [ @@ -66,7 +68,9 @@ const PrimeNGModules = [ CalendarModule, FileUploadModule, SelectButtonModule, -] + TabViewModule, + RadioButtonModule +]; @NgModule({ declarations: [ @@ -79,7 +83,7 @@ const PrimeNGModules = [ HideableHeaderComponent, MultiSelectColumnsComponent, FeatureFlagListComponent, - DataImportAndExportComponent, + DataImportAndExportComponent ], imports: [ CommonModule, diff --git a/web/src/app/modules/view/server/scheduled-events/components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component.html b/web/src/app/modules/view/server/scheduled-events/components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component.html new file mode 100644 index 00000000..938281d5 --- /dev/null +++ b/web/src/app/modules/view/server/scheduled-events/components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component.html @@ -0,0 +1,44 @@ +
+ +
+ + +
+ + +
+
+ +

+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem + aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. + Nemo + enim + ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos + qui + ratione voluptatem sequi nesciunt. Consectetur, adipisci velit, sed quia non numquam eius modi. +

+
+
+ +
+
+ +
+
+ + + +
+
+
+
+
+
diff --git a/web/src/app/modules/view/server/scheduled-events/components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component.scss b/web/src/app/modules/view/server/scheduled-events/components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/web/src/app/modules/view/server/scheduled-events/components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component.spec.ts b/web/src/app/modules/view/server/scheduled-events/components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component.spec.ts new file mode 100644 index 00000000..e97349b3 --- /dev/null +++ b/web/src/app/modules/view/server/scheduled-events/components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditScheduledEventDialogComponent } from './edit-scheduled-event-dialog.component'; + +describe('EditScheduledEventDialogComponent', () => { + let component: EditScheduledEventDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EditScheduledEventDialogComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(EditScheduledEventDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/web/src/app/modules/view/server/scheduled-events/components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component.ts b/web/src/app/modules/view/server/scheduled-events/components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component.ts new file mode 100644 index 00000000..ca5ff6d6 --- /dev/null +++ b/web/src/app/modules/view/server/scheduled-events/components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component.ts @@ -0,0 +1,70 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { EventType, ScheduledEvent } from "../../../../../../models/data/scheduled_events.model"; +import { TranslateService } from "@ngx-translate/core"; +import { FormBuilder, FormControl, Validators } from "@angular/forms"; + +@Component({ + selector: "app-edit-scheduled-event-dialog", + templateUrl: "./edit-scheduled-event-dialog.component.html", + styleUrls: ["./edit-scheduled-event-dialog.component.scss"] +}) +export class EditScheduledEventDialogComponent { + @Input() event?: ScheduledEvent; + @Output() save = new EventEmitter(); + + get visible() { + return this.event != undefined; + } + + set visible(val: boolean) { + if (!val) { + this.event = undefined; + } + this.visible = val; + } + + get header() { + if (this.event && this.event.createdAt === "" && this.event.modifiedAt === "") { + return this.translate.instant("view.server.scheduled_events.edit_dialog.add_header"); + } else { + return this.translate.instant("view.server.scheduled_events.edit_dialog.edit_header"); + } + } + + public activeIndex: number = 0; + public inputForm = this.fb.group({ + id: new FormControl(this.event?.id), + interval: new FormControl(this.event?.interval, [Validators.required]), + entityType: new FormControl(this.event?.entityType, [Validators.required]), + channelId: new FormControl(this.event?.channelId, this.event?.entityType == EventType.voice || this.event?.entityType == EventType.stageInstance ? [Validators.required] : []), + location: new FormControl(this.event?.location, this.event?.entityType == EventType.external ? [Validators.required] : []), + name: new FormControl(this.event?.name, [Validators.required]), + startTime: new FormControl(this.event?.startTime, [Validators.required]), + endTime: new FormControl(this.event?.endTime), + description: new FormControl(this.event?.description) + }); + + constructor( + private translate: TranslateService, + private fb: FormBuilder + ) { + } + + public saveEvent() { + this.save.emit(this.event); + } + + public next() { + this.activeIndex++; + } + + public back() { + this.activeIndex--; + } + + protected readonly EventType = { + stage: EventType.stageInstance, + voice: EventType.voice, + somewhere_else: EventType.external + }; +} diff --git a/web/src/app/modules/view/server/scheduled-events/components/scheduled-events/scheduled-events.component.html b/web/src/app/modules/view/server/scheduled-events/components/scheduled-events/scheduled-events.component.html new file mode 100644 index 00000000..1296b1bd --- /dev/null +++ b/web/src/app/modules/view/server/scheduled-events/components/scheduled-events/scheduled-events.component.html @@ -0,0 +1,307 @@ + + +

+ {{'view.server.scheduled_events.header' | translate}} +

+
+
+ + + +
+
+
+ {{scheduledEvents.length}} {{'common.of' | translate}} + {{dt.totalRecords}} + + {{'view.server.scheduled_events.scheduled_events' | translate}} +
+ + +
+ +
+ + + +
+
+
+ + + + +
+
{{'common.id' | translate}}
+ +
+ + + +
+
{{'common.interval' | translate}}
+ +
+ + +
+
{{'common.name' | translate}}
+ +
+ + +
+
{{'common.description' | translate}}
+ +
+ + +
+
{{'common.channel_id' | translate}}
+ +
+ + +
+
{{'common.start_time' | translate}}
+ +
+ + +
+
{{'common.end_time' | translate}}
+ +
+ + +
+
{{'common.type' | translate}}
+ +
+ + +
+
{{'common.location' | translate}}
+ +
+ + + +
+
{{'common.created_at' | translate}}
+
+ + + +
+
{{'common.modified_at' | translate}}
+
+ + + +
+
{{'common.actions' | translate}}
+
+ + + + + +
+ +
+ + + + + +
+ +
+ + + + + + + + + + + + +
+ + + + + {{'common.id' | translate}}: + + + {{scheduledEvent.id}} + + + {{scheduledEvent.id}} + + + + + + {{'common.interval' | translate}}: + + + + + + {{scheduledEvent.interval}} + + + + + + {{'common.name' | translate}}: + + + + + + {{scheduledEvent.name}} + + + + + + {{'common.description' | translate}}: + + + + + + {{scheduledEvent.description}} + + + + + + {{'common.channel_id' | translate}}: + + + + + + {{scheduledEvent.channel_id}} + + + + + + {{'common.start_time' | translate}}: + + + + + + {{scheduledEvent.start_time}} + + + + + + {{'common.end_time' | translate}}: + + + + + + {{scheduledEvent.end_time}} + + + + + + {{'common.type' | translate}}: + + + + + + {{scheduledEvent.type}} + + + + + + {{'common.location' | translate}}: + + + + + + {{scheduledEvent.location}} + + + + + + {{'common.created_at' | translate}}: + + + {{scheduledEvent.createdAt | date:'dd.MM.yy HH:mm'}} + + + {{scheduledEvent.createdAt | date:'dd.MM.yy HH:mm'}} + + + + + {{'common.modified_at' | translate}}: + + + {{scheduledEvent.modifiedAt | date:'dd.MM.yy HH:mm'}} + + + {{scheduledEvent.modifiedAt | date:'dd.MM.yy HH:mm'}} + + + + +
+ + + +
+ + +
+ + + + + {{'common.no_entries_found' | translate}} + + + + + + +
+
+
+ diff --git a/web/src/app/modules/view/server/scheduled-events/components/scheduled-events/scheduled-events.component.scss b/web/src/app/modules/view/server/scheduled-events/components/scheduled-events/scheduled-events.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/web/src/app/modules/view/server/scheduled-events/components/scheduled-events/scheduled-events.component.spec.ts b/web/src/app/modules/view/server/scheduled-events/components/scheduled-events/scheduled-events.component.spec.ts new file mode 100644 index 00000000..f7d3244f --- /dev/null +++ b/web/src/app/modules/view/server/scheduled-events/components/scheduled-events/scheduled-events.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ScheduledEventsComponent } from './scheduled-events.component'; + +describe('ScheduledEventsComponent', () => { + let component: ScheduledEventsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ScheduledEventsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ScheduledEventsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/web/src/app/modules/view/server/scheduled-events/components/scheduled-events/scheduled-events.component.ts b/web/src/app/modules/view/server/scheduled-events/components/scheduled-events/scheduled-events.component.ts new file mode 100644 index 00000000..da0e8972 --- /dev/null +++ b/web/src/app/modules/view/server/scheduled-events/components/scheduled-events/scheduled-events.component.ts @@ -0,0 +1,287 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, FormControl, FormGroup } from "@angular/forms"; +import { Page } from "../../../../../../models/graphql/filter/page.model"; +import { Sort, SortDirection } from "../../../../../../models/graphql/filter/sort.model"; +import { Subject, throwError } from "rxjs"; +import { Server } from "../../../../../../models/data/server.model"; +import { UserDTO } from "../../../../../../models/auth/auth-user.dto"; +import { LazyLoadEvent } from "primeng/api"; +import { Queries } from "../../../../../../models/graphql/queries.model"; +import { AuthService } from "../../../../../../services/auth/auth.service"; +import { SpinnerService } from "../../../../../../services/spinner/spinner.service"; +import { ToastService } from "../../../../../../services/toast/toast.service"; +import { ConfirmationDialogService } from "../../../../../../services/confirmation-dialog/confirmation-dialog.service"; +import { TranslateService } from "@ngx-translate/core"; +import { DataService } from "../../../../../../services/data/data.service"; +import { SidebarService } from "../../../../../../services/sidebar/sidebar.service"; +import { ActivatedRoute } from "@angular/router"; +import { Query, ScheduledEventListQuery } from "../../../../../../models/graphql/query.model"; +import { catchError, debounceTime, takeUntil } from "rxjs/operators"; +import { Table } from "primeng/table"; +import { ScheduledEventMutationResult } from "../../../../../../models/graphql/result.model"; +import { Mutations } from "../../../../../../models/graphql/mutations.model"; +import { ComponentWithTable } from "../../../../../../base/component-with-table"; +import { EventType, ScheduledEvent, ScheduledEventFilter } from "../../../../../../models/data/scheduled_events.model"; + +@Component({ + selector: "app-scheduled-events", + templateUrl: "./scheduled-events.component.html", + styleUrls: ["./scheduled-events.component.scss"] +}) +export class ScheduledEventsComponent extends ComponentWithTable implements OnInit, OnDestroy { + public scheduledEvents: ScheduledEvent[] = []; + public loading = true; + + public filterForm!: FormGroup<{ + id: FormControl, + interval: FormControl, + name: FormControl, + description: FormControl, + channelId: FormControl, + startTime: FormControl, + endTime: FormControl, + entityType: FormControl, + location: FormControl, + }>; + + public filter: ScheduledEventFilter = {}; + public page: Page = { + pageSize: undefined, + pageIndex: undefined + }; + public sort: Sort = { + sortColumn: undefined, + sortDirection: undefined + }; + + public totalRecords: number = 0; + + private unsubscriber = new Subject(); + private server: Server = {}; + public user: UserDTO | null = null; + public query: string = Queries.scheduledEventWithHistoryQuery; + public editableScheduledEvent?: ScheduledEvent = undefined; + + public constructor( + private authService: AuthService, + private spinner: SpinnerService, + private toastService: ToastService, + private confirmDialog: ConfirmationDialogService, + private fb: FormBuilder, + private translate: TranslateService, + private data: DataService, + private sidebar: SidebarService, + private route: ActivatedRoute) { + super("ScheduledEvent", ["id", "interval", "name", "description", "channel_id", "start_time", "end_time", "type", "location"], + (oldElement: ScheduledEvent, newElement: ScheduledEvent) => { + return oldElement.name === newElement.name; + }); + } + + public ngOnInit(): void { + this.loading = true; + this.setFilterForm(); + this.data.getServerFromRoute(this.route).then(async server => { + this.server = server; + let authUser = await this.authService.getLoggedInUser(); + this.user = authUser?.users?.find(u => u.server == this.server.id) ?? null; + }); + } + + public ngOnDestroy(): void { + this.unsubscriber.next(); + this.unsubscriber.complete(); + } + + public loadNextPage(): void { + this.data.query(Queries.scheduledEventQuery, { + serverId: this.server.id, filter: this.filter, page: this.page, sort: this.sort + }, + (data: Query) => { + return data.servers[0]; + } + ).subscribe(data => { + this.totalRecords = data.scheduledEventCount; + this.scheduledEvents = data.scheduledEvents; + this.spinner.hideSpinner(); + this.loading = false; + }); + } + + public setFilterForm(): void { + this.filterForm = this.fb.group({ + id: new FormControl(null), + interval: new FormControl(null), + name: new FormControl(null), + description: new FormControl(null), + channelId: new FormControl(null), + startTime: new FormControl(null), + endTime: new FormControl(null), + entityType: new FormControl(null), + location: new FormControl(null), + }); + + this.filterForm.valueChanges.pipe( + takeUntil(this.unsubscriber), + debounceTime(600) + ).subscribe(changes => { + if (changes.id) { + this.filter.id = changes.id; + } else { + this.filter.id = undefined; + } + + if (changes.interval) { + this.filter.interval = changes.interval; + } else { + this.filter.interval = undefined; + } + + if (changes.name) { + this.filter.name = changes.name; + } else { + this.filter.name = undefined; + } + + if (changes.description) { + this.filter.description = changes.description; + } else { + this.filter.description = undefined; + } + + if (changes.channelId) { + this.filter.channelId = changes.channelId; + } else { + this.filter.channelId = undefined; + } + + if (changes.startTime) { + this.filter.startTime = changes.startTime; + } else { + this.filter.startTime = undefined; + } + + if (changes.endTime) { + this.filter.endTime = changes.endTime; + } else { + this.filter.endTime = undefined; + } + + if (changes.entityType) { + this.filter.entityType = changes.entityType; + } else { + this.filter.entityType = undefined; + } + + if (changes.location) { + this.filter.location = changes.location; + } else { + this.filter.location = undefined; + } + + if (this.page.pageSize) + this.page.pageSize = 10; + + if (this.page.pageIndex) + this.page.pageIndex = 0; + + this.loadNextPage(); + }); + } + + public newScheduledEventTemplate: ScheduledEvent = { + createdAt: "", + modifiedAt: "" + }; + + public nextPage(event: LazyLoadEvent): void { + this.page.pageSize = event.rows ?? 0; + if (event.first != null && event.rows != null) + this.page.pageIndex = event.first / event.rows; + this.sort.sortColumn = event.sortField ?? undefined; + this.sort.sortDirection = event.sortOrder === 1 ? SortDirection.ASC : event.sortOrder === -1 ? SortDirection.DESC : SortDirection.ASC; + + this.loadNextPage(); + } + + public resetFilters(): void { + this.filterForm.reset(); + } + + public onRowEditInit(table: Table, event: ScheduledEvent, index: number): void { + this.editableScheduledEvent = event; + } + + public override onRowEditSave(newScheduledEvent: ScheduledEvent): void { + if (this.isEditingNew && JSON.stringify(newScheduledEvent) === JSON.stringify(this.newScheduledEventTemplate)) { + this.isEditingNew = false; + return; + } + + if (this.isEditingNew) { + this.spinner.showSpinner(); + this.data.mutation(Mutations.createScheduledEvent, { + name: newScheduledEvent.name, + serverId: this.server.id + } + ).pipe(catchError(err => { + this.isEditingNew = false; + this.spinner.hideSpinner(); + return throwError(err); + })).subscribe(result => { + this.isEditingNew = false; + this.spinner.hideSpinner(); + this.toastService.success(this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_create"), this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_create_d", { name: result.scheduledEvent.createScheduledEvent?.name })); + this.loadNextPage(); + }); + return; + } + + this.spinner.showSpinner(); + this.data.mutation(Mutations.updateScheduledEvent, { + id: newScheduledEvent.id, + name: newScheduledEvent.name, + } + ).pipe(catchError(err => { + this.spinner.hideSpinner(); + return throwError(err); + })).subscribe(_ => { + this.spinner.hideSpinner(); + this.toastService.success(this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_update"), this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_update_d", { name: newScheduledEvent.name })); + this.loadNextPage(); + }); + } + + public deleteScheduledEvent(ScheduledEvent: ScheduledEvent): void { + this.confirmDialog.confirmDialog( + this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_delete"), this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_delete_q", { name: ScheduledEvent.name }), + () => { + this.spinner.showSpinner(); + this.data.mutation(Mutations.deleteScheduledEvent, { + id: ScheduledEvent.id + } + ).pipe(catchError(err => { + this.spinner.hideSpinner(); + return throwError(err); + })).subscribe(l => { + this.spinner.hideSpinner(); + this.toastService.success(this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_deleted"), this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_deleted_d", { name: ScheduledEvent.name })); + this.loadNextPage(); + }); + }); + } + + public addScheduledEvent(table: Table): void { + this.editableScheduledEvent = JSON.parse(JSON.stringify(this.newScheduledEventTemplate)); + // const newScheduledEvent = JSON.parse(JSON.stringify(this.newScheduledEventTemplate)); + // + // this.scheduledEvents = [newScheduledEvent, ...this.scheduledEvents]; + // + // table.initRowEdit(newScheduledEvent); + // + // const index = this.scheduledEvents.findIndex(l => l.id == newScheduledEvent.id); + // this.onRowEditInit(table, newScheduledEvent, index); + // + // this.isEditingNew = true; + } +} diff --git a/web/src/app/modules/view/server/scheduled-events/scheduled-events-routing.module.ts b/web/src/app/modules/view/server/scheduled-events/scheduled-events-routing.module.ts new file mode 100644 index 00000000..d0712636 --- /dev/null +++ b/web/src/app/modules/view/server/scheduled-events/scheduled-events-routing.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; +import { ScheduledEventsComponent } from "./components/scheduled-events/scheduled-events.component"; + +const routes: Routes = [ + { path: "", component: ScheduledEventsComponent } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ScheduledEventsRoutingModule { +} diff --git a/web/src/app/modules/view/server/scheduled-events/scheduled-events.module.ts b/web/src/app/modules/view/server/scheduled-events/scheduled-events.module.ts new file mode 100644 index 00000000..752b6ce7 --- /dev/null +++ b/web/src/app/modules/view/server/scheduled-events/scheduled-events.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { ScheduledEventsRoutingModule } from './scheduled-events-routing.module'; +import { ScheduledEventsComponent } from "./components/scheduled-events/scheduled-events.component"; +import { SharedModule } from "../../../shared/shared.module"; +import { EditScheduledEventDialogComponent } from './components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component'; + + +@NgModule({ + declarations: [ + ScheduledEventsComponent, + EditScheduledEventDialogComponent + ], + imports: [ + CommonModule, + SharedModule, + ScheduledEventsRoutingModule + ] +}) +export class ScheduledEventsModule { } diff --git a/web/src/app/modules/view/server/server-routing.module.ts b/web/src/app/modules/view/server/server-routing.module.ts index 505aa609..310e44a5 100644 --- a/web/src/app/modules/view/server/server-routing.module.ts +++ b/web/src/app/modules/view/server/server-routing.module.ts @@ -19,6 +19,7 @@ const routes: Routes = [ { path: "levels", loadChildren: () => import("./levels/levels.module").then(m => m.LevelsModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Moderator } }, { path: "achievements", loadChildren: () => import("./achievements/achievements.module").then(m => m.AchievementsModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Moderator } }, { path: "short-role-names", loadChildren: () => import("./short-role-name/short-role-name.module").then(m => m.ShortRoleNameModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Moderator } }, + { path: "scheduled-events", loadChildren: () => import("./scheduled-events/scheduled-events.module").then(m => m.ScheduledEventsModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Moderator } }, { path: "config", loadChildren: () => import("./config/config.module").then(m => m.ConfigModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Admin } } ]; diff --git a/web/src/app/modules/view/server/short-role-name/short-role-name.module.ts b/web/src/app/modules/view/server/short-role-name/short-role-name.module.ts index b675df35..dd6e101b 100644 --- a/web/src/app/modules/view/server/short-role-name/short-role-name.module.ts +++ b/web/src/app/modules/view/server/short-role-name/short-role-name.module.ts @@ -1,14 +1,8 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { ShortRoleNamesComponent } from './components/short-role-names/short-role-names.component'; +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { ShortRoleNamesComponent } from "./components/short-role-names/short-role-names.component"; import { ShortRoleNameRoutingModule } from "./short-role-name-routing.module"; -import { ButtonModule } from "primeng/button"; -import { InputTextModule } from "primeng/inputtext"; -import { ReactiveFormsModule } from "@angular/forms"; import { SharedModule } from "../../../shared/shared.module"; -import { TableModule } from "primeng/table"; -import { TranslateModule } from "@ngx-translate/core"; - @NgModule({ @@ -18,13 +12,7 @@ import { TranslateModule } from "@ngx-translate/core"; imports: [ CommonModule, ShortRoleNameRoutingModule, - ButtonModule, - InputTextModule, - ReactiveFormsModule, SharedModule, - SharedModule, - TableModule, - TranslateModule ] }) export class ShortRoleNameModule { } diff --git a/web/src/app/services/sidebar/sidebar.service.ts b/web/src/app/services/sidebar/sidebar.service.ts index 2a1f3861..285b3a8f 100644 --- a/web/src/app/services/sidebar/sidebar.service.ts +++ b/web/src/app/services/sidebar/sidebar.service.ts @@ -30,6 +30,7 @@ export class SidebarService { serverAutoRoles: MenuItem = {}; serverLevels: MenuItem = {}; serverAchievements: MenuItem = {}; + serverScheduledEvents: MenuItem = {}; serverShortRoleNames: MenuItem = {}; serverConfig: MenuItem = {}; serverMenu: MenuItem = {}; @@ -110,6 +111,13 @@ export class SidebarService { routerLink: `server/${this.server?.id}/achievements` }; + this.serverScheduledEvents = { + label: this.isSidebarOpen ? this.translateService.instant("sidebar.server.scheduled_events") : "", + icon: "pi pi-calender", + visible: true, + routerLink: `server/${this.server?.id}/scheduled-events` + }; + this.serverShortRoleNames = { label: this.isSidebarOpen ? this.translateService.instant("sidebar.server.short_role_names") : "", icon: "pi pi-list", @@ -129,7 +137,7 @@ export class SidebarService { icon: "pi pi-server", visible: false, expanded: true, - items: [this.serverDashboard, this.serverProfile, this.serverMembers, this.serverAutoRoles, this.serverLevels, this.serverAchievements, this.serverShortRoleNames, this.serverConfig] + items: [this.serverDashboard, this.serverProfile, this.serverMembers, this.serverAutoRoles, this.serverLevels, this.serverAchievements, this.serverScheduledEvents, this.serverShortRoleNames, this.serverConfig] }; this.adminConfig = { label: this.isSidebarOpen ? this.translateService.instant("sidebar.config") : "", @@ -205,6 +213,7 @@ export class SidebarService { this.serverAutoRoles.visible = isTechnicianAndFullAccessActive || this.hasFeature("AutoRoleModule") && user?.isModerator; this.serverLevels.visible = isTechnicianAndFullAccessActive || this.hasFeature("LevelModule") && user?.isModerator; this.serverAchievements.visible = isTechnicianAndFullAccessActive || this.hasFeature("AchievementsModule") && user?.isModerator; + this.serverScheduledEvents.visible = isTechnicianAndFullAccessActive || this.hasFeature("ScheduledEvents") && user?.isModerator; this.serverShortRoleNames.visible = isTechnicianAndFullAccessActive || this.hasFeature("ShortRoleName") && user?.isAdmin; this.serverConfig.visible = isTechnicianAndFullAccessActive || user?.isAdmin; diff --git a/web/src/assets/i18n/de.json b/web/src/assets/i18n/de.json index e8a6e082..845e8a17 100644 --- a/web/src/assets/i18n/de.json +++ b/web/src/assets/i18n/de.json @@ -124,12 +124,14 @@ }, "common": { "404": "404 - Der Eintrag konnte nicht gefunden werden", + "abort": "Abbrechen", "actions": "Aktionen", "active": "Aktiv", "add": "Hinzufügen", "attribute": "Attribut", "auth_role": "Rolle", "author": "Autor", + "back": "Zurück", "bool_as_string": { "false": "Nein", "true": "Ja" @@ -137,12 +139,14 @@ "channel_id": "Kanal Id", "channel_name": "Kanal", "color": "Farbe", + "continue": "Weiter", "created_at": "Erstellt am", "description": "Beschreibung", "discord_id": "Discord Id", "edit": "Bearbeiten", "email": "E-Mail", "emoji": "Emoji", + "end_time": "Endzeit", "error": "Fehler", "export": "Exportieren", "feature_flags": "Funktionen", @@ -176,11 +180,13 @@ }, "id": "Id", "import": "Importieren", + "interval": "Interval", "joined_at": "Beigetreten am", "last_name": "Nachname", "leaved_at": "Verlassen am", "left_server": "Aktiv", "level": "Level", + "location": "Ort", "message_id": "Nachricht Id", "min_xp": "Min. XP", "modified_at": "Bearbeitet am", @@ -200,10 +206,12 @@ "role": "Rolle", "rule_count": "Regeln", "save": "Speichern", + "start_time": "Startzeit", "state": { "off": "Aus", "on": "Ein" }, + "type": "Typ", "user_warnings": "Verwarnungen", "users": "Benutzer", "value": "Wert", @@ -538,6 +546,33 @@ "reaction_count": "Anzahl Reaktionen", "xp": "XP" }, + "scheduled_events": { + "edit_dialog": { + "add_header": "Event hinzufügen", + "edit_header": "Event bearbeiten", + "event_info": { + "description_input": "Erzähl den Leuten ein wenig mehr über dein Event. Markdown, neue Zeilen und Links werden unterstützt.", + "event_topic": "Thema", + "event_topic_input": "Worum geht es bei deinem Event?", + "header": "Worum geht es bei deinem Event?", + "start_date": "Startdatum", + "start_time": "Startzeit", + "tab_name": "Eventinformationen" + }, + "location": { + "header": "Wo ist dein Event?", + "somewhere_else": "Irgendwo anders", + "somewhere_else_input": "Ort eingeben", + "stage": "Stage-Kanal", + "stage_input": "Stage-Kanal auswählen", + "tab_name": "Verzeichnis", + "voice": "Sprachkanal", + "voice_input": "Sprachkanal auswählen" + } + }, + "header": "Geplante Events", + "scheduled_events": "Geplante Events" + }, "short_role_names": { "header": "Rollen Kürzel", "message": { diff --git a/web/src/assets/i18n/en.json b/web/src/assets/i18n/en.json index 4f324ec6..86a3cd2a 100644 --- a/web/src/assets/i18n/en.json +++ b/web/src/assets/i18n/en.json @@ -124,12 +124,14 @@ }, "common": { "404": "404 - Entry not found!", + "abort": "Abort", "actions": "Actions", "active": "Active", "add": "Add", "attribute": "Attribute", "auth_role": "Role", "author": "Author", + "back": "Back", "bool_as_string": { "false": "No", "true": "Yes" @@ -137,6 +139,7 @@ "channel_id": "Channel Id", "channel_name": "Channel", "color": "Color", + "continue": "Continue", "created_at": "Created at", "description": "Description", "discord_id": "Discord Id", @@ -538,6 +541,33 @@ "reaction_count": "Reaction count", "xp": "XP" }, + "scheduled_events": { + "edit_dialog": { + "add_header": "Add event", + "edit_header": "Edit event", + "event_info": { + "description_input": "Tell people a little more about your event. Markdown, new lines and links are supported.", + "event_topic": "Event topic", + "event_topic_input": "What's your event?", + "header": "What's your event about?", + "start_date": "Start date", + "start_time": "Start time", + "tab_name": "Event info" + }, + "location": { + "header": "Where is your event?", + "somewhere_else": "Somewhere else", + "somewhere_else_input": "Enter a location", + "stage": "Stage channel", + "stage_input": "Select a stage channel", + "tab_name": "Location", + "voice": "Voice channel", + "voice_input": "Select a voice channel" + } + }, + "header": "Scheduled events", + "scheduled_events": "Scheduled events" + }, "short_role_names": { "header": "Level", "message": { diff --git a/web/src/styles.scss b/web/src/styles.scss index 64751ea5..3878c311 100644 --- a/web/src/styles.scss +++ b/web/src/styles.scss @@ -635,6 +635,18 @@ footer { border: 0; } +.btn, +.icon-btn, +.text-btn, +.danger-btn, +.danger-icon-btn { + span { + transition-duration: unset !important; + } + + transition: none !important; +} + .spinner-component-wrapper { position: absolute; top: 0; @@ -661,6 +673,27 @@ p-inputNumber { border: none !important; } +.edit-dialog { + .p-dialog-content { + padding: 0 !important; + + .p-tabview-nav { + justify-content: space-between; + + li { + width: 100%; + + a { + width: 100%; + justify-content: center; + } + } + } + + + } +} + @media (max-width: 720px) { footer { .left, diff --git a/web/src/styles/themes/sh-edraft-dark-theme.scss b/web/src/styles/themes/sh-edraft-dark-theme.scss index 6f483f02..f7b2f949 100644 --- a/web/src/styles/themes/sh-edraft-dark-theme.scss +++ b/web/src/styles/themes/sh-edraft-dark-theme.scss @@ -552,6 +552,7 @@ } } + .danger-btn, .danger-icon-btn { background-color: transparent !important; color: $primaryTextColor !important; @@ -568,22 +569,6 @@ } } - .danger-btn { - background-color: $primaryErrorColor !important; - color: $primaryErrorColor !important; - border: 0 !important; - - &:hover { - background-color: $primaryErrorColor !important; - color: $primaryTextColor !important; - border: 0; - } - - .pi { - font-size: 1.275rem !important; - } - } - .p-datatable .p-sortable-column.p-highlight, .p-datatable .p-sortable-column.p-highlight .p-sortable-column-icon, .p-datatable .p-sortable-column:not(.p-highlight):hover { @@ -777,4 +762,26 @@ } } } + + .edit-dialog { + .p-dialog-content { + .p-tabview { + .p-tabview-nav li .p-tabview-nav-link:not(.p-disabled):focus { + box-shadow: none !important; + } + + .p-tabview-nav li.p-highlight .p-tabview-nav-link { + color: $primaryHeaderColor !important; + border-color: $primaryHeaderColor !important; + } + + .p-tabview-nav, + .p-tabview-nav li .p-tabview-nav-link, + .p-tabview-panels { + background-color: $secondaryBackgroundColor !important; + color: $primaryTextColor !important; + } + } + } + } }