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 @@
+
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}}
+
+
+
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;
+ }
+ }
+ }
+ }
}