diff --git a/admin/src/app/app.routes.ts b/admin/src/app/app.routes.ts
index aca0b8f..d24a682 100644
--- a/admin/src/app/app.routes.ts
+++ b/admin/src/app/app.routes.ts
@@ -21,8 +21,55 @@ import { UserFormComponent } from './features/user/components/user-form/user-for
import { UserDetailsComponent } from './features/user/components/user-details/user-details.component';
import { UserTableComponent } from './features/user/components/user-table/user-table.component';
import { UserListComponent } from './features/user/components/user-list/user-list.component';
+import { UserGroupFormComponent } from './features/user-group/components/user-group-form/user-group-form.component';
+import {
+ UserGroupDetailsComponent,
+} from './features/user-group/components/user-group-details/user-group-details.component';
+import { UserGroupTableComponent } from './features/user-group/components/user-group-table/user-group-table.component';
+import { UserGroupListComponent } from './features/user-group/components/user-group-list/user-group-list.component';
export const routes: Routes = [
+ {
+ path: 'user-group/new',
+ component: UserGroupFormComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+ {
+ path: 'user-group',
+ component: UserGroupListComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+ {
+ path: 'user-group/table',
+ component: UserGroupTableComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+ {
+ path: 'user-group/:id',
+ component: UserGroupDetailsComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+ {
+ path: 'user-group/:id/edit',
+ component: UserGroupFormComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+
{
path: 'user/new',
component: UserFormComponent,
diff --git a/admin/src/app/app.ts b/admin/src/app/app.ts
index 6773b62..27243b1 100644
--- a/admin/src/app/app.ts
+++ b/admin/src/app/app.ts
@@ -44,7 +44,14 @@ export class App {
`
- }
+ },{
+ menuText: 'Felhasználó Csoport',
+ targetUrl: '/user-group/table',
+ svgIcon: `
+`
+ },
]
diff --git a/admin/src/app/features/user-group/components/user-group-details/user-group-details.component.html b/admin/src/app/features/user-group/components/user-group-details/user-group-details.component.html
new file mode 100644
index 0000000..9e2410f
--- /dev/null
+++ b/admin/src/app/features/user-group/components/user-group-details/user-group-details.component.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
UserGroup Details
+
+
+
+
+
+ | id |
+ {{ userGroup.id }} |
+
+
+ | name |
+ {{ userGroup.name }} |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/src/app/features/user-group/components/user-group-details/user-group-details.component.ts b/admin/src/app/features/user-group/components/user-group-details/user-group-details.component.ts
new file mode 100644
index 0000000..299e564
--- /dev/null
+++ b/admin/src/app/features/user-group/components/user-group-details/user-group-details.component.ts
@@ -0,0 +1,34 @@
+// dvbooking-cli/src/templates/angular/details.component.ts.tpl
+
+// Generated by the CLI
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ActivatedRoute, RouterModule } from '@angular/router';
+import { Observable } from 'rxjs';
+import { switchMap } from 'rxjs/operators';
+import { UserGroup } from '../../models/user-group.model';
+import { UserGroupService } from '../../services/user-group.service';
+
+@Component({
+ selector: 'app-user-group-details',
+ templateUrl: './user-group-details.component.html',
+ standalone: true,
+ imports: [CommonModule, RouterModule],
+})
+export class UserGroupDetailsComponent implements OnInit {
+ userGroup$!: Observable;
+
+ constructor(
+ private route: ActivatedRoute,
+ private userGroupService: UserGroupService
+ ) {}
+
+ ngOnInit(): void {
+ this.userGroup$ = this.route.params.pipe(
+ switchMap(params => {
+ const id = params['id'];
+ return this.userGroupService.findOne(id);
+ })
+ );
+ }
+}
\ No newline at end of file
diff --git a/admin/src/app/features/user-group/components/user-group-filter/user-group-filter.component.html b/admin/src/app/features/user-group/components/user-group-filter/user-group-filter.component.html
new file mode 100644
index 0000000..5e3ee19
--- /dev/null
+++ b/admin/src/app/features/user-group/components/user-group-filter/user-group-filter.component.html
@@ -0,0 +1,12 @@
+
+
+
\ No newline at end of file
diff --git a/admin/src/app/features/user-group/components/user-group-filter/user-group-filter.component.ts b/admin/src/app/features/user-group/components/user-group-filter/user-group-filter.component.ts
new file mode 100644
index 0000000..05d106b
--- /dev/null
+++ b/admin/src/app/features/user-group/components/user-group-filter/user-group-filter.component.ts
@@ -0,0 +1,37 @@
+// dvbooking-cli/src/templates/angular/filter.component.ts.tpl
+
+// Generated by the CLI
+import { Component, EventEmitter, Output } from '@angular/core';
+import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
+
+@Component({
+ selector: 'app-user-group-filter',
+ templateUrl: './user-group-filter.component.html',
+ standalone: true,
+ imports: [ReactiveFormsModule]
+})
+export class UserGroupFilterComponent {
+ @Output() filterChanged = new EventEmitter();
+ filterForm: FormGroup;
+
+ constructor(private fb: FormBuilder) {
+ this.filterForm = this.fb.group({
+ name: ['']
+ });
+
+ this.filterForm.valueChanges.pipe(
+ debounceTime(300),
+ distinctUntilChanged()
+ ).subscribe(values => {
+ const cleanFilter = Object.fromEntries(
+ Object.entries(values).filter(([_, v]) => v != null && v !== '')
+ );
+ this.filterChanged.emit(cleanFilter);
+ });
+ }
+
+ reset() {
+ this.filterForm.reset();
+ }
+}
\ No newline at end of file
diff --git a/admin/src/app/features/user-group/components/user-group-form/user-group-form.component.html b/admin/src/app/features/user-group/components/user-group-form/user-group-form.component.html
new file mode 100644
index 0000000..87e81f4
--- /dev/null
+++ b/admin/src/app/features/user-group/components/user-group-form/user-group-form.component.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ {{ isEditMode ? 'Edit' : 'Create' }} UserGroup
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/src/app/features/user-group/components/user-group-form/user-group-form.component.ts b/admin/src/app/features/user-group/components/user-group-form/user-group-form.component.ts
new file mode 100644
index 0000000..8a701cb
--- /dev/null
+++ b/admin/src/app/features/user-group/components/user-group-form/user-group-form.component.ts
@@ -0,0 +1,85 @@
+// dvbooking-cli/src/templates/angular/form.component.ts.tpl
+
+// Generated by the CLI
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
+import { ActivatedRoute, Router, RouterModule } from '@angular/router';
+import { Observable, of } from 'rxjs';
+import { switchMap, tap } from 'rxjs/operators';
+import { UserGroup } from '../../models/user-group.model';
+import { UserGroupService } from '../../services/user-group.service';
+
+@Component({
+ selector: 'app-user-group-form',
+ templateUrl: './user-group-form.component.html',
+ standalone: true,
+ imports: [CommonModule, ReactiveFormsModule, RouterModule],
+})
+export class UserGroupFormComponent implements OnInit {
+ form: FormGroup;
+ isEditMode = false;
+ id: number | null = null;
+
+ private numericFields = [];
+
+ constructor(
+ private fb: FormBuilder,
+ private route: ActivatedRoute,
+ private router: Router,
+ private userGroupService: UserGroupService
+ ) {
+ this.form = this.fb.group({
+ name: [null]
+ });
+ }
+
+ ngOnInit(): void {
+ this.route.params.pipe(
+ tap(params => {
+ if (params['id']) {
+ this.isEditMode = true;
+ this.id = +params['id'];
+ }
+ }),
+ switchMap(() => {
+ if (this.isEditMode && this.id) {
+ return this.userGroupService.findOne(this.id);
+ }
+ return of(null);
+ })
+ ).subscribe(userGroup => {
+ if (userGroup) {
+ this.form.patchValue(userGroup);
+ }
+ });
+ }
+
+ onSubmit(): void {
+ if (this.form.invalid) {
+ this.form.markAllAsTouched();
+ return;
+ }
+
+ const payload = { ...this.form.value };
+
+ for (const field of this.numericFields) {
+ if (payload[field] != null && payload[field] !== '') {
+ payload[field] = parseFloat(payload[field]);
+ }
+ }
+
+ let action$: Observable;
+
+ if (this.isEditMode && this.id) {
+ action$ = this.userGroupService.update(this.id, payload);
+ } else {
+ action$ = this.userGroupService.create(payload);
+ }
+
+ action$.subscribe({
+ next: () => this.router.navigate(['/user-group']),
+ error: (err) => console.error('Failed to save user-group', err)
+ });
+ }
+}
\ No newline at end of file
diff --git a/admin/src/app/features/user-group/components/user-group-list/user-group-list.component.html b/admin/src/app/features/user-group/components/user-group-list/user-group-list.component.html
new file mode 100644
index 0000000..2e37ada
--- /dev/null
+++ b/admin/src/app/features/user-group/components/user-group-list/user-group-list.component.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | id |
+ name |
+ Actions |
+
+
+
+
+ | {{ item.id }} |
+ {{ item.name }} |
+
+ View
+ Edit
+
+ |
+
+
+ | No user-group found. |
+
+
+
+
+
+
+ 1" class="flex justify-center mt-4">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/src/app/features/user-group/components/user-group-list/user-group-list.component.ts b/admin/src/app/features/user-group/components/user-group-list/user-group-list.component.ts
new file mode 100644
index 0000000..493537f
--- /dev/null
+++ b/admin/src/app/features/user-group/components/user-group-list/user-group-list.component.ts
@@ -0,0 +1,70 @@
+// dvbooking-cli/src/templates/angular/list.component.ts.tpl
+
+// Generated by the CLI
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { RouterModule } from '@angular/router';
+import { BehaviorSubject, Observable, combineLatest } from 'rxjs';
+import { switchMap, startWith } from 'rxjs/operators';
+import { UserGroup } from '../../models/user-group.model';
+import { UserGroupService } from '../../services/user-group.service';
+import { UserGroupFilterComponent } from '../user-group-filter/user-group-filter.component';
+import { PaginatedResponse } from '../../../../../types';
+
+
+@Component({
+ selector: 'app-user-group-list',
+ templateUrl: './user-group-list.component.html',
+ standalone: true,
+ imports: [CommonModule,RouterModule, UserGroupFilterComponent],
+})
+export class UserGroupListComponent implements OnInit {
+
+ private refresh$ = new BehaviorSubject(undefined);
+ private filter$ = new BehaviorSubject({});
+ private page$ = new BehaviorSubject(1);
+
+ paginatedResponse$!: Observable>;
+
+ constructor(private userGroupService: UserGroupService) { }
+
+ ngOnInit(): void {
+ this.paginatedResponse$ = combineLatest([
+ this.refresh$,
+ this.filter$.pipe(startWith({})),
+ this.page$.pipe(startWith(1))
+ ]).pipe(
+ switchMap(([_, filter, page]) => {
+ const query = { ...filter, page, limit: 10 };
+ return this.userGroupService.find(query);
+ })
+ );
+ }
+
+ onFilterChanged(filter: any): void {
+ this.page$.next(1);
+ this.filter$.next(filter);
+ }
+
+ changePage(newPage: number): void {
+ if (newPage > 0) {
+ this.page$.next(newPage);
+ }
+ }
+
+ deleteItem(id: number): void {
+ if (confirm('Are you sure you want to delete this item?')) {
+ this.userGroupService.remove(id).subscribe({
+ next: () => {
+ console.log(`Item with ID ${id} deleted successfully.`);
+ this.refresh$.next();
+ },
+ // --- THIS IS THE FIX ---
+ // Explicitly type 'err' to satisfy strict TypeScript rules.
+ error: (err: any) => {
+ console.error(`Error deleting item with ID ${id}:`, err);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/admin/src/app/features/user-group/components/user-group-table/user-group-data-provider.service.ts b/admin/src/app/features/user-group/components/user-group-table/user-group-data-provider.service.ts
new file mode 100644
index 0000000..2a7f1d6
--- /dev/null
+++ b/admin/src/app/features/user-group/components/user-group-table/user-group-data-provider.service.ts
@@ -0,0 +1,26 @@
+// dvbooking-cli/src/templates/angular-generic/data-provider.service.ts.tpl
+
+// Generated by the CLI
+import { inject, Injectable } from '@angular/core';
+import { DataProvider, GetDataOptions, GetDataResponse } from '../../../../components/generic-table/data-provider.interface';
+import { UserGroup } from '../../models/user-group.model';
+import { map, Observable } from 'rxjs';
+import { UserGroupService } from '../../services/user-group.service';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class UserGroupDataProvider implements DataProvider {
+ private userGroupService = inject(UserGroupService);
+
+ getData(options?: GetDataOptions): Observable> {
+ const {q,page,limit} = options?.params ?? {};
+ // The generic table's params are compatible with our NestJS Query DTO
+ return this.userGroupService.search(q ?? '',page,limit, ).pipe(
+ map((res) => {
+ // Adapt the paginated response to the GetDataResponse format
+ return { data: res };
+ })
+ );
+ }
+}
\ No newline at end of file
diff --git a/admin/src/app/features/user-group/components/user-group-table/user-group-table.component.html b/admin/src/app/features/user-group/components/user-group-table/user-group-table.component.html
new file mode 100644
index 0000000..385969f
--- /dev/null
+++ b/admin/src/app/features/user-group/components/user-group-table/user-group-table.component.html
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/admin/src/app/features/user-group/components/user-group-table/user-group-table.component.ts b/admin/src/app/features/user-group/components/user-group-table/user-group-table.component.ts
new file mode 100644
index 0000000..c54d50e
--- /dev/null
+++ b/admin/src/app/features/user-group/components/user-group-table/user-group-table.component.ts
@@ -0,0 +1,100 @@
+// dvbooking-cli/src/templates/angular-generic/table.component.ts.tpl
+
+// Generated by the CLI
+import { Component, inject, OnInit } from '@angular/core';
+import { Router, RouterModule } from '@angular/router';
+import { UserGroup } from '../../models/user-group.model';
+import { UserGroupDataProvider } from './user-group-data-provider.service';
+import { ColumnDefinition } from '../../../../components/generic-table/column-definition.interface';
+import { GenericTable } from '../../../../components/generic-table/generic-table';
+import { GenericTableConfig } from '../../../../components/generic-table/generic-table.config';
+import {
+ ActionDefinition,
+ GenericActionColumn,
+} from '../../../../components/generic-action-column/generic-action-column';
+import { UserGroupService } from '../../services/user-group.service';
+import { BehaviorSubject } from 'rxjs';
+
+@Component({
+ selector: 'app-user-group-table',
+ standalone: true,
+ imports: [GenericTable, RouterModule],
+ templateUrl: './user-group-table.component.html',
+})
+export class UserGroupTableComponent implements OnInit {
+
+ private refresh$ = new BehaviorSubject(undefined);
+ private filter$ = new BehaviorSubject({});
+ private page$ = new BehaviorSubject(1);
+ private limit$ = new BehaviorSubject(10);
+
+ router = inject(Router);
+ tableConfig!: GenericTableConfig;
+
+ userGroupDataProvider = inject(UserGroupDataProvider);
+ userGroupService = inject(UserGroupService);
+
+ ngOnInit(): void {
+ const actionHandler = (action: ActionDefinition, item: UserGroup) => {
+ switch (action.action) {
+ case 'view':
+ this.router.navigate(['/user-group', item?.id]);
+ break;
+ case 'edit':
+ this.router.navigate(['/user-group', item?.id, 'edit']);
+ break;
+ case 'delete':
+ this.deleteItem(item.id);
+ break;
+ }
+ };
+
+ this.tableConfig = {
+ refresh$: this.refresh$,
+ filter$: this.filter$,
+ page$: this.page$,
+ limit$: this.limit$,
+ dataProvider: this.userGroupDataProvider,
+ columns: [
+ {
+ attribute: 'name',
+ headerCell: true,
+ valueCell: true,
+ },
+ {
+ attribute: 'actions',
+ headerCell: { value: 'Actions' },
+ valueCell: {
+ component: GenericActionColumn,
+ componentInputs: item => ({
+ item: item,
+ actions: [
+ { action: 'view', handler: actionHandler },
+ { action: 'edit', handler: actionHandler },
+ { action: 'delete', handler: actionHandler },
+ ] as ActionDefinition[],
+ }),
+ },
+ },
+ ] as ColumnDefinition[],
+ tableCssClass: 'user-group-table-container',
+ };
+ }
+
+ deleteItem(id: number): void {
+ if (confirm('Are you sure you want to delete this item?')) {
+ this.userGroupService.remove(id).subscribe({
+ next: () => {
+ console.log(`Item with ID ${id} deleted successfully.`);
+ this.refresh$.next();
+ },
+ // --- THIS IS THE FIX ---
+ // Explicitly type 'err' to satisfy strict TypeScript rules.
+ error: (err: any) => {
+ console.error(`Error deleting item with ID ${id}:`, err);
+ }
+ });
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/admin/src/app/features/user-group/models/user-group.model.ts b/admin/src/app/features/user-group/models/user-group.model.ts
new file mode 100644
index 0000000..3fd57a0
--- /dev/null
+++ b/admin/src/app/features/user-group/models/user-group.model.ts
@@ -0,0 +1,7 @@
+// dvbooking-cli/src/templates/angular/model.ts.tpl
+
+// Generated by the CLI
+export interface UserGroup {
+ id: number;
+ name: string;
+}
diff --git a/admin/src/app/features/user-group/services/user-group.service.ts b/admin/src/app/features/user-group/services/user-group.service.ts
new file mode 100644
index 0000000..1f06303
--- /dev/null
+++ b/admin/src/app/features/user-group/services/user-group.service.ts
@@ -0,0 +1,85 @@
+// dvbooking-cli/src/templates/angular/service.ts.tpl
+
+// Generated by the CLI
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { UserGroup } from '../models/user-group.model';
+import { ConfigurationService } from '../../../services/configuration.service';
+import { PaginatedResponse } from '../../../../types';
+
+
+export interface SearchResponse {
+ data: T[];
+ total: number;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UserGroupService {
+ private readonly apiUrl: string;
+
+ constructor(
+ private http: HttpClient,
+ private configService: ConfigurationService
+ ) {
+ this.apiUrl = `${this.configService.getApiUrl()}/user-group`;
+ }
+
+ /**
+ * Find records with pagination and filtering.
+ */
+ public find(filter: Record): Observable> {
+ // --- THIS IS THE FIX ---
+ // The incorrect line: .filter(([_, v]) for v != null)
+ // is now correctly written with an arrow function.
+ const cleanFilter = Object.fromEntries(
+ Object.entries(filter).filter(([_, v]) => v != null)
+ );
+ // --- END OF FIX ---
+
+ const params = new HttpParams({ fromObject: cleanFilter });
+ return this.http.get>(this.apiUrl, { params });
+ }
+
+ /**
+ * Search across multiple fields with a single term.
+ * @param term The search term (q).
+ */
+ public search(term: string, page: number = 1, limit: number = 10): Observable> {
+ const params = new HttpParams()
+ .set('q', term)
+ .set('page', page.toString())
+ .set('limit', limit.toString());
+ return this.http.get>(`${this.apiUrl}/search`, { params });
+ }
+
+ /**
+ * Find a single record by its ID.
+ */
+ public findOne(id: number): Observable {
+ return this.http.get(`${this.apiUrl}/${id}`);
+ }
+
+ /**
+ * Create a new record.
+ */
+ public create(data: Omit): Observable {
+ return this.http.post(this.apiUrl, data);
+ }
+
+ /**
+ * Update an existing record.
+ */
+ public update(id: number, data: Partial>): Observable {
+ return this.http.patch(`${this.apiUrl}/${id}`, data);
+ }
+
+ /**
+ * Remove a record by its ID.
+ */
+ public remove(id: number): Observable {
+ return this.http.delete(`${this.apiUrl}/${id}`);
+ }
+}
\ No newline at end of file
diff --git a/server/src/app.module.ts b/server/src/app.module.ts
index 2618fea..910e0ef 100644
--- a/server/src/app.module.ts
+++ b/server/src/app.module.ts
@@ -15,6 +15,7 @@ import { ProductsModule } from './product/products.module';
import { Event } from './entity/event.entity';
import { EventsModule } from './event/events.module';
import { User } from './entity/user';
+import { UserGroupsModule } from './user-group/user-group.module';
const moduleTypeOrm = TypeOrmModule.forRootAsync({
imports: [ConfigModule],
@@ -44,6 +45,7 @@ const moduleTypeOrm = TypeOrmModule.forRootAsync({
EventTypesModule,
ProductsModule,
EventsModule,
+ UserGroupsModule,
],
controllers: [AppController],
providers: [AppService],
diff --git a/server/src/entity/user-group.ts b/server/src/entity/user-group.ts
index 3993e01..3b7ee90 100644
--- a/server/src/entity/user-group.ts
+++ b/server/src/entity/user-group.ts
@@ -6,6 +6,7 @@ import {
JoinTable,
} from 'typeorm';
import { UserRole } from './user-role';
+import { IsString } from 'class-validator';
@Entity()
export class UserGroup {
@@ -13,6 +14,7 @@ export class UserGroup {
id: number;
@Column({ unique: true })
+ @IsString()
name: string;
@ManyToMany(() => UserRole)
diff --git a/server/src/user-group/dto/create-user-group.dto.ts b/server/src/user-group/dto/create-user-group.dto.ts
new file mode 100644
index 0000000..e55fd5d
--- /dev/null
+++ b/server/src/user-group/dto/create-user-group.dto.ts
@@ -0,0 +1,4 @@
+import { OmitType } from '@nestjs/mapped-types';
+import { UserGroup } from '../../entity/user-group';
+
+export class CreateUserGroupDto extends OmitType(UserGroup, ['id']) {}
\ No newline at end of file
diff --git a/server/src/user-group/dto/query-user-group.dto.ts b/server/src/user-group/dto/query-user-group.dto.ts
new file mode 100644
index 0000000..0d7fa92
--- /dev/null
+++ b/server/src/user-group/dto/query-user-group.dto.ts
@@ -0,0 +1,14 @@
+import {
+ IsOptional,
+ IsString,
+ IsNumber,
+ IsIn,
+} from 'class-validator';
+import { Type } from 'class-transformer';
+
+export class QueryUserGroupDto {
+ @IsOptional() @Type(() => Number) @IsNumber() page?: number;
+ @IsOptional() @Type(() => Number) @IsNumber() limit?: number;
+ @IsOptional() @IsString() sortBy?: string;
+ @IsOptional() @IsIn(['ASC', 'DESC']) order?: 'ASC' | 'DESC';
+}
\ No newline at end of file
diff --git a/server/src/user-group/dto/update-user-group.dto.ts b/server/src/user-group/dto/update-user-group.dto.ts
new file mode 100644
index 0000000..bb5753b
--- /dev/null
+++ b/server/src/user-group/dto/update-user-group.dto.ts
@@ -0,0 +1,4 @@
+import { PartialType } from '@nestjs/mapped-types';
+import { CreateUserGroupDto } from './create-user-group.dto';
+
+export class UpdateUserGroupDto extends PartialType(CreateUserGroupDto) {}
\ No newline at end of file
diff --git a/server/src/user-group/user-group.controller.ts b/server/src/user-group/user-group.controller.ts
new file mode 100644
index 0000000..39b5045
--- /dev/null
+++ b/server/src/user-group/user-group.controller.ts
@@ -0,0 +1,63 @@
+import {
+ Controller,
+ Get,
+ Post,
+ Body,
+ Patch,
+ Param,
+ Delete,
+ Query,
+ ParseIntPipe,
+ DefaultValuePipe,
+ UseGuards,
+} from '@nestjs/common';
+import { UserGroupsService } from './user-group.service';
+import { CreateUserGroupDto } from './dto/create-user-group.dto';
+import { UpdateUserGroupDto } from './dto/update-user-group.dto';
+import { QueryUserGroupDto } from './dto/query-user-group.dto';
+
+import { JwtAuthGuard } from '../auth/jwt-auth.guard';
+import { Roles } from '../auth/roles.decorator';
+import { Role } from '../auth/role.enum';
+import { RolesGuard } from '../auth/roles.guard';
+
+@Controller('user-group')
+@UseGuards(JwtAuthGuard, RolesGuard)
+@Roles(Role.Admin)
+export class UserGroupsController {
+ constructor(private readonly userGroupsService: UserGroupsService) {}
+
+ @Post()
+ create(@Body() createUserGroupDto: CreateUserGroupDto) {
+ return this.userGroupsService.create(createUserGroupDto);
+ }
+
+ @Get()
+ findAll(@Query() queryParams: QueryUserGroupDto) {
+ return this.userGroupsService.findAll(queryParams);
+ }
+
+ @Get('search')
+ search(
+ @Query('q') term: string,
+ @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
+ @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
+ ) {
+ return this.userGroupsService.search(term, { page, limit });
+ }
+
+ @Get(':id')
+ findOne(@Param('id', ParseIntPipe) id: number) {
+ return this.userGroupsService.findOne(id);
+ }
+
+ @Patch(':id')
+ update(@Param('id', ParseIntPipe) id: number, @Body() updateUserGroupDto: UpdateUserGroupDto) {
+ return this.userGroupsService.update(id, updateUserGroupDto);
+ }
+
+ @Delete(':id')
+ remove(@Param('id', ParseIntPipe) id: number) {
+ return this.userGroupsService.remove(id);
+ }
+}
\ No newline at end of file
diff --git a/server/src/user-group/user-group.module.ts b/server/src/user-group/user-group.module.ts
new file mode 100644
index 0000000..019b63e
--- /dev/null
+++ b/server/src/user-group/user-group.module.ts
@@ -0,0 +1,13 @@
+import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { UserGroupsService } from './user-group.service';
+import { UserGroupsController } from './user-group.controller';
+import { UserGroup } from '../entity/user-group';
+
+
+@Module({
+ imports: [TypeOrmModule.forFeature([UserGroup])],
+ controllers: [UserGroupsController],
+ providers: [UserGroupsService],
+})
+export class UserGroupsModule {}
\ No newline at end of file
diff --git a/server/src/user-group/user-group.service.ts b/server/src/user-group/user-group.service.ts
new file mode 100644
index 0000000..494060c
--- /dev/null
+++ b/server/src/user-group/user-group.service.ts
@@ -0,0 +1,101 @@
+import { Injectable, NotFoundException } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository, FindManyOptions, FindOptionsWhere, ILike } from 'typeorm';
+import { CreateUserGroupDto } from './dto/create-user-group.dto';
+import { UpdateUserGroupDto } from './dto/update-user-group.dto';
+import { QueryUserGroupDto } from './dto/query-user-group.dto';
+import { UserGroup } from '../entity/user-group';
+
+type QueryConfigItem = {
+ param: keyof Omit;
+ dbField: keyof UserGroup;
+ operator: 'equals' | 'like';
+};
+
+@Injectable()
+export class UserGroupsService {
+ constructor(
+ @InjectRepository(UserGroup)
+ private readonly userGroupRepository: Repository,
+ ) {}
+
+ private readonly searchableFields: (keyof UserGroup)[] = [
+ 'name'
+ ];
+
+ create(createUserGroupDto: CreateUserGroupDto) {
+ const newRecord = this.userGroupRepository.create(createUserGroupDto);
+ return this.userGroupRepository.save(newRecord);
+ }
+
+ async findAll(queryParams: QueryUserGroupDto) {
+ const { page = 1, limit = 0, sortBy, order, ...filters } = queryParams;
+ const queryConfig: QueryConfigItem[] = [];
+ const whereClause: { [key: string]: any } = {};
+ for (const config of queryConfig) {
+ if (filters[config.param] !== undefined) {
+ if (config.operator === 'like') {
+ whereClause[config.dbField] = ILike(`%${filters[config.param]}%`);
+ } else {
+ whereClause[config.dbField] = filters[config.param];
+ }
+ }
+ }
+ const findOptions: FindManyOptions = { where: whereClause as FindOptionsWhere };
+ const paginated = limit > 0;
+ if (paginated) {
+ findOptions.skip = (page - 1) * limit;
+ findOptions.take = limit;
+ }
+ if (sortBy && order) {
+ findOptions.order = { [sortBy]: order };
+ }
+ const [data, totalItems] = await this.userGroupRepository.findAndCount(findOptions);
+ if (!paginated) {
+ return { data, total: data.length };
+ }
+ return {
+ data,
+ meta: { totalItems, itemCount: data.length, itemsPerPage: limit, totalPages: Math.ceil(totalItems / limit), currentPage: page },
+ };
+ }
+
+ async search(term: string, options: { page: number; limit: number }) {
+ if (this.searchableFields.length === 0) {
+ console.warn('Search is not configured for this entity.');
+ return { data: [], meta: { totalItems: 0, itemCount: 0, itemsPerPage: options.limit, totalPages: 0, currentPage: options.page } };
+ }
+ const whereConditions = this.searchableFields.map(field => ({ [field]: ILike(`%${term}%`) }));
+ const [data, totalItems] = await this.userGroupRepository.findAndCount({
+ where: whereConditions,
+ skip: (options.page - 1) * options.limit,
+ take: options.limit,
+ });
+ return {
+ data,
+ meta: { totalItems, itemCount: data.length, itemsPerPage: options.limit, totalPages: Math.ceil(totalItems / options.limit), currentPage: options.page },
+ };
+ }
+
+ async findOne(id: number) {
+ const record = await this.userGroupRepository.findOneBy({ id: id as any });
+ if (!record) {
+ throw new NotFoundException(`UserGroup with ID ${id} not found`);
+ }
+ return record;
+ }
+
+ async update(id: number, updateUserGroupDto: UpdateUserGroupDto) {
+ const record = await this.findOne(id);
+ Object.assign(record, updateUserGroupDto);
+ return this.userGroupRepository.save(record);
+ }
+
+ async remove(id: number) {
+ const result = await this.userGroupRepository.delete(id);
+ if (result.affected === 0) {
+ throw new NotFoundException(`UserGroup with ID ${id} not found`);
+ }
+ return { deleted: true, id };
+ }
+}
\ No newline at end of file