diff --git a/admin/src/app/app.routes.ts b/admin/src/app/app.routes.ts
index d24a682..676c0fa 100644
--- a/admin/src/app/app.routes.ts
+++ b/admin/src/app/app.routes.ts
@@ -27,8 +27,54 @@ import {
} 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';
+import { UserRoleFormComponent } from './features/user-role/components/user-role-form/user-role-form.component';
+import {
+ UserRoleDetailsComponent,
+} from './features/user-role/components/user-role-details/user-role-details.component';
+import { UserRoleTableComponent } from './features/user-role/components/user-role-table/user-role-table.component';
+import { UserRoleListComponent } from './features/user-role/components/user-role-list/user-role-list.component';
export const routes: Routes = [
+ {
+ path: 'user-role/new',
+ component: UserRoleFormComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+ {
+ path: 'user-role',
+ component: UserRoleListComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+ {
+ path: 'user-role/table',
+ component: UserRoleTableComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+ {
+ path: 'user-role/:id',
+ component: UserRoleDetailsComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+ {
+ path: 'user-role/:id/edit',
+ component: UserRoleFormComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
{
path: 'user-group/new',
component: UserGroupFormComponent,
diff --git a/admin/src/app/app.ts b/admin/src/app/app.ts
index 27243b1..9a7bb70 100644
--- a/admin/src/app/app.ts
+++ b/admin/src/app/app.ts
@@ -44,12 +44,21 @@ export class App {
`
- },{
+ },
+ {
menuText: 'Felhasználó Csoport',
targetUrl: '/user-group/table',
svgIcon: `
+`
+ },
+{
+ menuText: 'Felhasználó Szerepek',
+ targetUrl: '/user-role/table',
+ svgIcon: `
`
},
diff --git a/admin/src/app/features/user-role/components/user-role-details/user-role-details.component.html b/admin/src/app/features/user-role/components/user-role-details/user-role-details.component.html
new file mode 100644
index 0000000..8fe59b1
--- /dev/null
+++ b/admin/src/app/features/user-role/components/user-role-details/user-role-details.component.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
UserRole Details
+
+
+
+
+
+ | id |
+ {{ userRole.id }} |
+
+
+ | name |
+ {{ userRole.name }} |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/src/app/features/user-role/components/user-role-details/user-role-details.component.ts b/admin/src/app/features/user-role/components/user-role-details/user-role-details.component.ts
new file mode 100644
index 0000000..0711c4e
--- /dev/null
+++ b/admin/src/app/features/user-role/components/user-role-details/user-role-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 { UserRole } from '../../models/user-role.model';
+import { UserRoleService } from '../../services/user-role.service';
+
+@Component({
+ selector: 'app-user-role-details',
+ templateUrl: './user-role-details.component.html',
+ standalone: true,
+ imports: [CommonModule, RouterModule],
+})
+export class UserRoleDetailsComponent implements OnInit {
+ userRole$!: Observable;
+
+ constructor(
+ private route: ActivatedRoute,
+ private userRoleService: UserRoleService
+ ) {}
+
+ ngOnInit(): void {
+ this.userRole$ = this.route.params.pipe(
+ switchMap(params => {
+ const id = params['id'];
+ return this.userRoleService.findOne(id);
+ })
+ );
+ }
+}
\ No newline at end of file
diff --git a/admin/src/app/features/user-role/components/user-role-filter/user-role-filter.component.html b/admin/src/app/features/user-role/components/user-role-filter/user-role-filter.component.html
new file mode 100644
index 0000000..5e3ee19
--- /dev/null
+++ b/admin/src/app/features/user-role/components/user-role-filter/user-role-filter.component.html
@@ -0,0 +1,12 @@
+
+
+
\ No newline at end of file
diff --git a/admin/src/app/features/user-role/components/user-role-filter/user-role-filter.component.ts b/admin/src/app/features/user-role/components/user-role-filter/user-role-filter.component.ts
new file mode 100644
index 0000000..4f5a00c
--- /dev/null
+++ b/admin/src/app/features/user-role/components/user-role-filter/user-role-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-role-filter',
+ templateUrl: './user-role-filter.component.html',
+ standalone: true,
+ imports: [ReactiveFormsModule]
+})
+export class UserRoleFilterComponent {
+ @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-role/components/user-role-form/user-role-form.component.html b/admin/src/app/features/user-role/components/user-role-form/user-role-form.component.html
new file mode 100644
index 0000000..cbfef34
--- /dev/null
+++ b/admin/src/app/features/user-role/components/user-role-form/user-role-form.component.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ {{ isEditMode ? 'Edit' : 'Create' }} UserRole
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/src/app/features/user-role/components/user-role-form/user-role-form.component.ts b/admin/src/app/features/user-role/components/user-role-form/user-role-form.component.ts
new file mode 100644
index 0000000..39c1ce3
--- /dev/null
+++ b/admin/src/app/features/user-role/components/user-role-form/user-role-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 { UserRole } from '../../models/user-role.model';
+import { UserRoleService } from '../../services/user-role.service';
+
+@Component({
+ selector: 'app-user-role-form',
+ templateUrl: './user-role-form.component.html',
+ standalone: true,
+ imports: [CommonModule, ReactiveFormsModule, RouterModule],
+})
+export class UserRoleFormComponent implements OnInit {
+ form: FormGroup;
+ isEditMode = false;
+ id: number | null = null;
+
+ private numericFields = [];
+
+ constructor(
+ private fb: FormBuilder,
+ private route: ActivatedRoute,
+ private router: Router,
+ private userRoleService: UserRoleService
+ ) {
+ 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.userRoleService.findOne(this.id);
+ }
+ return of(null);
+ })
+ ).subscribe(userRole => {
+ if (userRole) {
+ this.form.patchValue(userRole);
+ }
+ });
+ }
+
+ 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.userRoleService.update(this.id, payload);
+ } else {
+ action$ = this.userRoleService.create(payload);
+ }
+
+ action$.subscribe({
+ next: () => this.router.navigate(['/user-role']),
+ error: (err) => console.error('Failed to save user-role', err)
+ });
+ }
+}
\ No newline at end of file
diff --git a/admin/src/app/features/user-role/components/user-role-list/user-role-list.component.html b/admin/src/app/features/user-role/components/user-role-list/user-role-list.component.html
new file mode 100644
index 0000000..801b7ff
--- /dev/null
+++ b/admin/src/app/features/user-role/components/user-role-list/user-role-list.component.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | id |
+ name |
+ Actions |
+
+
+
+
+ | {{ item.id }} |
+ {{ item.name }} |
+
+ View
+ Edit
+
+ |
+
+
+ | No user-role found. |
+
+
+
+
+
+
+ 1" class="flex justify-center mt-4">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/src/app/features/user-role/components/user-role-list/user-role-list.component.ts b/admin/src/app/features/user-role/components/user-role-list/user-role-list.component.ts
new file mode 100644
index 0000000..a58ef7b
--- /dev/null
+++ b/admin/src/app/features/user-role/components/user-role-list/user-role-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 { UserRole } from '../../models/user-role.model';
+import { UserRoleService } from '../../services/user-role.service';
+import { UserRoleFilterComponent } from '../user-role-filter/user-role-filter.component';
+import { PaginatedResponse } from '../../../../../types';
+
+
+@Component({
+ selector: 'app-user-role-list',
+ templateUrl: './user-role-list.component.html',
+ standalone: true,
+ imports: [CommonModule,RouterModule, UserRoleFilterComponent],
+})
+export class UserRoleListComponent implements OnInit {
+
+ private refresh$ = new BehaviorSubject(undefined);
+ private filter$ = new BehaviorSubject({});
+ private page$ = new BehaviorSubject(1);
+
+ paginatedResponse$!: Observable>;
+
+ constructor(private userRoleService: UserRoleService) { }
+
+ 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.userRoleService.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.userRoleService.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-role/components/user-role-table/user-role-data-provider.service.ts b/admin/src/app/features/user-role/components/user-role-table/user-role-data-provider.service.ts
new file mode 100644
index 0000000..1be78d5
--- /dev/null
+++ b/admin/src/app/features/user-role/components/user-role-table/user-role-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 { UserRole } from '../../models/user-role.model';
+import { map, Observable } from 'rxjs';
+import { UserRoleService } from '../../services/user-role.service';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class UserRoleDataProvider implements DataProvider {
+ private userRoleService = inject(UserRoleService);
+
+ getData(options?: GetDataOptions): Observable> {
+ const {q,page,limit} = options?.params ?? {};
+ // The generic table's params are compatible with our NestJS Query DTO
+ return this.userRoleService.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-role/components/user-role-table/user-role-table.component.html b/admin/src/app/features/user-role/components/user-role-table/user-role-table.component.html
new file mode 100644
index 0000000..436c136
--- /dev/null
+++ b/admin/src/app/features/user-role/components/user-role-table/user-role-table.component.html
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/admin/src/app/features/user-role/components/user-role-table/user-role-table.component.ts b/admin/src/app/features/user-role/components/user-role-table/user-role-table.component.ts
new file mode 100644
index 0000000..3175cc7
--- /dev/null
+++ b/admin/src/app/features/user-role/components/user-role-table/user-role-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 { UserRole } from '../../models/user-role.model';
+import { UserRoleDataProvider } from './user-role-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 { UserRoleService } from '../../services/user-role.service';
+import { BehaviorSubject } from 'rxjs';
+
+@Component({
+ selector: 'app-user-role-table',
+ standalone: true,
+ imports: [GenericTable, RouterModule],
+ templateUrl: './user-role-table.component.html',
+})
+export class UserRoleTableComponent 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;
+
+ userRoleDataProvider = inject(UserRoleDataProvider);
+ userRoleService = inject(UserRoleService);
+
+ ngOnInit(): void {
+ const actionHandler = (action: ActionDefinition, item: UserRole) => {
+ switch (action.action) {
+ case 'view':
+ this.router.navigate(['/user-role', item?.id]);
+ break;
+ case 'edit':
+ this.router.navigate(['/user-role', 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.userRoleDataProvider,
+ 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-role-table-container',
+ };
+ }
+
+ deleteItem(id: number): void {
+ if (confirm('Are you sure you want to delete this item?')) {
+ this.userRoleService.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-role/models/user-role.model.ts b/admin/src/app/features/user-role/models/user-role.model.ts
new file mode 100644
index 0000000..4dbba33
--- /dev/null
+++ b/admin/src/app/features/user-role/models/user-role.model.ts
@@ -0,0 +1,7 @@
+// dvbooking-cli/src/templates/angular/model.ts.tpl
+
+// Generated by the CLI
+export interface UserRole {
+ id: number;
+ name: string;
+}
diff --git a/admin/src/app/features/user-role/services/user-role.service.ts b/admin/src/app/features/user-role/services/user-role.service.ts
new file mode 100644
index 0000000..a096301
--- /dev/null
+++ b/admin/src/app/features/user-role/services/user-role.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 { UserRole } from '../models/user-role.model';
+import { ConfigurationService } from '../../../services/configuration.service';
+import { PaginatedResponse } from '../../../../types';
+
+
+export interface SearchResponse {
+ data: T[];
+ total: number;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UserRoleService {
+ private readonly apiUrl: string;
+
+ constructor(
+ private http: HttpClient,
+ private configService: ConfigurationService
+ ) {
+ this.apiUrl = `${this.configService.getApiUrl()}/user-role`;
+ }
+
+ /**
+ * 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 910e0ef..7a65adf 100644
--- a/server/src/app.module.ts
+++ b/server/src/app.module.ts
@@ -16,6 +16,7 @@ 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';
+import { UserRolesModule } from './user-role/user-role.module';
const moduleTypeOrm = TypeOrmModule.forRootAsync({
imports: [ConfigModule],
@@ -46,6 +47,7 @@ const moduleTypeOrm = TypeOrmModule.forRootAsync({
ProductsModule,
EventsModule,
UserGroupsModule,
+ UserRolesModule,
],
controllers: [AppController],
providers: [AppService],
diff --git a/server/src/user-role/dto/create-user-role.dto.ts b/server/src/user-role/dto/create-user-role.dto.ts
new file mode 100644
index 0000000..f0f652d
--- /dev/null
+++ b/server/src/user-role/dto/create-user-role.dto.ts
@@ -0,0 +1,4 @@
+import { OmitType } from '@nestjs/mapped-types';
+import { UserRole } from '../../entity/user-role';
+
+export class CreateUserRoleDto extends OmitType(UserRole, ['id']) {}
\ No newline at end of file
diff --git a/server/src/user-role/dto/query-user-role.dto.ts b/server/src/user-role/dto/query-user-role.dto.ts
new file mode 100644
index 0000000..b8cc919
--- /dev/null
+++ b/server/src/user-role/dto/query-user-role.dto.ts
@@ -0,0 +1,14 @@
+import {
+ IsOptional,
+ IsString,
+ IsNumber,
+ IsIn,
+} from 'class-validator';
+import { Type } from 'class-transformer';
+
+export class QueryUserRoleDto {
+ @IsOptional() @Type(() => Number) @IsNumber() page?: number;
+ @IsOptional() @Type(() => Number) @IsNumber() limit?: number;
+ @IsOptional() @IsString() sortBy?: string;
+ @IsOptional() @IsIn(['ASC', 'DESC']) order?: 'ASC' | 'DESC';
+}
diff --git a/server/src/user-role/dto/update-user-role.dto.ts b/server/src/user-role/dto/update-user-role.dto.ts
new file mode 100644
index 0000000..0e31631
--- /dev/null
+++ b/server/src/user-role/dto/update-user-role.dto.ts
@@ -0,0 +1,4 @@
+import { PartialType } from '@nestjs/mapped-types';
+import { CreateUserRoleDto } from './create-user-role.dto';
+
+export class UpdateUserRoleDto extends PartialType(CreateUserRoleDto) {}
\ No newline at end of file
diff --git a/server/src/user-role/user-role.controller.ts b/server/src/user-role/user-role.controller.ts
new file mode 100644
index 0000000..8cf73f2
--- /dev/null
+++ b/server/src/user-role/user-role.controller.ts
@@ -0,0 +1,63 @@
+import {
+ Controller,
+ Get,
+ Post,
+ Body,
+ Patch,
+ Param,
+ Delete,
+ Query,
+ ParseIntPipe,
+ DefaultValuePipe,
+ UseGuards,
+} from '@nestjs/common';
+import { UserRolesService } from './user-role.service';
+import { CreateUserRoleDto } from './dto/create-user-role.dto';
+import { UpdateUserRoleDto } from './dto/update-user-role.dto';
+import { QueryUserRoleDto } from './dto/query-user-role.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-role')
+@UseGuards(JwtAuthGuard, RolesGuard)
+@Roles(Role.Admin)
+export class UserRolesController {
+ constructor(private readonly userRolesService: UserRolesService) {}
+
+ @Post()
+ create(@Body() createUserRoleDto: CreateUserRoleDto) {
+ return this.userRolesService.create(createUserRoleDto);
+ }
+
+ @Get()
+ findAll(@Query() queryParams: QueryUserRoleDto) {
+ return this.userRolesService.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.userRolesService.search(term, { page, limit });
+ }
+
+ @Get(':id')
+ findOne(@Param('id', ParseIntPipe) id: number) {
+ return this.userRolesService.findOne(id);
+ }
+
+ @Patch(':id')
+ update(@Param('id', ParseIntPipe) id: number, @Body() updateUserRoleDto: UpdateUserRoleDto) {
+ return this.userRolesService.update(id, updateUserRoleDto);
+ }
+
+ @Delete(':id')
+ remove(@Param('id', ParseIntPipe) id: number) {
+ return this.userRolesService.remove(id);
+ }
+}
\ No newline at end of file
diff --git a/server/src/user-role/user-role.module.ts b/server/src/user-role/user-role.module.ts
new file mode 100644
index 0000000..0d2d853
--- /dev/null
+++ b/server/src/user-role/user-role.module.ts
@@ -0,0 +1,12 @@
+import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { UserRolesService } from './user-role.service';
+import { UserRolesController } from './user-role.controller';
+import { UserRole } from '../entity/user-role';
+
+@Module({
+ imports: [TypeOrmModule.forFeature([UserRole])],
+ controllers: [UserRolesController],
+ providers: [UserRolesService],
+})
+export class UserRolesModule {}
\ No newline at end of file
diff --git a/server/src/user-role/user-role.service.ts b/server/src/user-role/user-role.service.ts
new file mode 100644
index 0000000..e658826
--- /dev/null
+++ b/server/src/user-role/user-role.service.ts
@@ -0,0 +1,125 @@
+import { Injectable, NotFoundException } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository, FindManyOptions, FindOptionsWhere, ILike } from 'typeorm';
+import { CreateUserRoleDto } from './dto/create-user-role.dto';
+import { UpdateUserRoleDto } from './dto/update-user-role.dto';
+import { QueryUserRoleDto } from './dto/query-user-role.dto';
+import { UserRole } from '../entity/user-role';
+
+type QueryConfigItem = {
+ param: keyof Omit;
+ dbField: keyof UserRole;
+ operator: 'equals' | 'like';
+};
+
+@Injectable()
+export class UserRolesService {
+ constructor(
+ @InjectRepository(UserRole)
+ private readonly userRoleRepository: Repository,
+ ) {}
+
+ private readonly searchableFields: (keyof UserRole)[] = ['name'];
+
+ create(createUserRoleDto: CreateUserRoleDto) {
+ const newRecord = this.userRoleRepository.create(createUserRoleDto);
+ return this.userRoleRepository.save(newRecord);
+ }
+
+ async findAll(queryParams: QueryUserRoleDto) {
+ 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.userRoleRepository.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.userRoleRepository.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.userRoleRepository.findOneBy({ id: id });
+ if (!record) {
+ throw new NotFoundException(`UserRole with ID ${id} not found`);
+ }
+ return record;
+ }
+
+ async update(id: number, updateUserRoleDto: UpdateUserRoleDto) {
+ const record = await this.findOne(id);
+ Object.assign(record, updateUserRoleDto);
+ return this.userRoleRepository.save(record);
+ }
+
+ async remove(id: number) {
+ const result = await this.userRoleRepository.delete(id);
+ if (result.affected === 0) {
+ throw new NotFoundException(`UserRole with ID ${id} not found`);
+ }
+ return { deleted: true, id };
+ }
+}