Implement admin and user sidebars with routing and styling updates

This commit is contained in:
liosha84
2025-07-19 17:23:11 +03:00
parent 70338eae0b
commit 88f0cf307e
26 changed files with 540 additions and 120 deletions
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { AdminModuleService } from './admin-module.service';
describe('AdminModuleService', () => {
let service: AdminModuleService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AdminModuleService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import {Observable} from 'rxjs';
import {HttpClient} from '@angular/common/http';
import {User} from '../models/user.model';
@Injectable({
providedIn: 'root'
})
export class AdminModuleService {
baseUrl = 'http://localhost:8080/api';
constructor(private http: HttpClient) {
}
updateUserRoles(id: any,data: User): Observable<any> {
return this.http.put(`${this.baseUrl}/users/${id}`, data);
}
}
@@ -1,28 +1,13 @@
<!--<p>admin.component works!</p>-->
<app-navigation
class="pc-sidebar"
[ngClass]="{
'navbar-collapsed': navCollapsed,
'mob-open': navCollapsedMob
}"
(NavCollapse)="this.navCollapsed = !this.navCollapsed"
/>
<app-nav-bar (NavCollapsedMob)="navMobClick()" (NavCollapse)="this.navCollapsed = !this.navCollapsed"/>
<app-side-bar-admin
class="pc-sidebar"
>
</app-side-bar-admin>
<div class="pc-container">
<div class="coded-wrapper">
<div class="coded-content">
<div class="coded-inner-content">
<app-breadcrumb />
<div class="main-body">
<div class="page-wrapper">
<router-outlet />
</div>
</div>
</div>
</div>
</div>
<div class="pc-menu-overlay" (click)="closeMenu()" (keydown)="handleKeyDown($event)" tabindex="0"></div>
</div>
@@ -1,22 +1,16 @@
.example-container {
width: 500px;
height: 300px;
border: 1px solid rgba(0, 0, 0, 0.5);
}
.example-sidenav-content {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
}
.example-sidenav {
padding: 20px;
}
//---------------
.pc-sidebar{
top: 120px;
top: 65px;
overflow-y: auto;
background-color: rgba(153, 153, 153, 0.16);
backdrop-filter: blur(8px);
}
.pc-container{
top: 0px;
padding-left: 5px;
padding-right: 5px;
}
@@ -9,6 +9,7 @@ import {MatButton} from '@angular/material/button';
import { ScrollPanelModule } from 'primeng/scrollpanel';
import {MenuItem} from 'primeng/api';
import {PanelMenu} from 'primeng/panelmenu';
import {SideBarAdminComponent} from '../side-bar-admin.component/side-bar-admin.component';
@Component({
selector: 'app-admin.component',
imports: [
@@ -24,6 +25,7 @@ import {PanelMenu} from 'primeng/panelmenu';
RouterLink,
PanelMenu,
NgStyle,
SideBarAdminComponent,
],
schemas:[CUSTOM_ELEMENTS_SCHEMA],
@@ -8,7 +8,7 @@ const ADMIN_ROUTES: Routes = [
children: [
{
path: 'welcome',
path: 'admin-welcome',
loadComponent: () => import('../admin-module/admin-welcome.component/admin-welcome.component').then((c) => c.AdminWelcomeComponent),
},
{
@@ -17,9 +17,12 @@ const ADMIN_ROUTES: Routes = [
},
{
path: 'system',
loadComponent: () => import('../admin-module/system.component/system.component').then((c) => c.SystemComponent)
}
},
{
path: 'users',
loadComponent: () => import('../admin-module/users.component/users.component').then((c) => c.UsersComponent)
}
]
}
];
@@ -0,0 +1,35 @@
<mat-nav-list>
<a mat-list-item routerLink="admin-welcome">
<span class="entry">
<mat-icon>house</mat-icon>
@if (!isCollapsed) {
<span >Dashboard</span>
}
</span>
</a>
<a mat-list-item routerLink="users">
<span class="entry">
<mat-icon>newspaper</mat-icon>
@if (!isCollapsed) {
<span >Users</span>
}
</span>
</a>
<a mat-list-item routerLink="settings">
<span class="entry">
<mat-icon>newspaper</mat-icon>
@if (!isCollapsed) {
<span >Settings</span>
}
</span>
</a>
<a mat-list-item routerLink="system">
<span class="entry">
<mat-icon>newspaper</mat-icon>
@if (!isCollapsed) {
<span >System</span>
}
</span>
</a>
</mat-nav-list>
@@ -0,0 +1,14 @@
.entry{
display: flex;
align-items: center;
gap: 1rem;
padding:0.75rem;
color: rgba(24, 255, 255, 0.96);
}
a.mdc-list-item
{
background-color: rgba(24,255,255,0.04);
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SideBarAdminComponent } from './side-bar-admin.component';
describe('SideBarAdminComponent', () => {
let component: SideBarAdminComponent;
let fixture: ComponentFixture<SideBarAdminComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SideBarAdminComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SideBarAdminComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,20 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {MatListItem, MatNavList} from '@angular/material/list';
import {RouterLink} from '@angular/router';
import {MatIcon} from '@angular/material/icon';
@Component({
selector: 'app-side-bar-admin',
imports: [
MatIcon,
MatListItem,
MatNavList,
RouterLink
],
templateUrl: './side-bar-admin.component.html',
styleUrl: './side-bar-admin.component.scss',
schemas:[CUSTOM_ELEMENTS_SCHEMA]
})
export class SideBarAdminComponent {
isCollapsed = false;
}
@@ -0,0 +1,56 @@
<table mat-table [dataSource]="users">
@for (column of columnsSchema; track column){
<ng-container [matColumnDef]="column.key">
<th mat-header-cell *matHeaderCellDef>
{{column.label}}
</th>
<td mat-cell *matCellDef="let element">
@switch (column.type) {
@case ('list') {
<mat-form-field class="example-chip-list">
<mat-chip-grid #chipGrid aria-label="Role selection">
@for (role of element.roles; track $index) {
<mat-chip-row (removed)="remove(role,element)">
{{role.name}}
<mat-icon
matChipRemove
[attr.aria-label]="'remove ' + role.name"
>
cancel
</mat-icon>
</mat-chip-row>
}
</mat-chip-grid>
<input
name="currentRole"
placeholder="Add role..."
[matChipInputFor]="chipGrid"
[matAutocomplete]="auto"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[formControl]="rolesControl"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="add($event,element)"
(input)="change($event,filteredRoles(element.roles))"
/>
<mat-autocomplete [formControl]="ac"
autoActiveFirstOption
#auto
(optionSelected)="selected(element,$event); ">
@for (role of filteredRoles(element.roles); track role) {
<mat-option [value]="role">{{role.name}}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
}
@default {
{{ element[column.key] }}
}
}
</td>
</ng-container>
}
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
@@ -0,0 +1,3 @@
.example-chip-list {
width: 100%;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UsersComponent } from './users.component';
describe('UsersComponent', () => {
let component: UsersComponent;
let fixture: ComponentFixture<UsersComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UsersComponent]
})
.compileComponents();
fixture = TestBed.createComponent(UsersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,185 @@
import {ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, model} from '@angular/core';
import {UserService} from '../../services/user.service';
import {RolesService} from '../../services/roles.service';
import {Role} from '../../models/role';
import {User} from '../../models/user.model';
import {
MatCell,
MatCellDef, MatColumnDef,
MatHeaderCell, MatHeaderCellDef,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef, MatTable
} from '@angular/material/table';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {
MatAutocomplete,
MatAutocompleteSelectedEvent,
MatAutocompleteTrigger,
MatOption
} from '@angular/material/autocomplete';
import {
MatChip,
MatChipGrid,
MatChipInput,
MatChipInputEvent,
MatChipRow,
MatChipsModule
} from '@angular/material/chips';
import {MatIcon} from '@angular/material/icon';
import {COMMA, ENTER} from '@angular/cdk/keycodes';
import {MatFormField} from '@angular/material/input';
import {MatSelect, MatSelectTrigger} from '@angular/material/select';
import {AdminModuleService} from '../admin-module.service';
@Component({
selector: 'app-users.component',
imports: [
MatCell,
MatCellDef,
MatHeaderCell,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef,
MatTable,
MatColumnDef,
MatHeaderCellDef,
FormsModule,
ReactiveFormsModule,
MatChipGrid,
MatChipRow,
MatIcon,
MatAutocompleteTrigger,
MatChipInput,
MatAutocomplete,
MatOption,
MatFormField,
MatSelect,
MatSelectTrigger,
MatChip,
MatChipsModule
],
templateUrl: './users.component.html',
styleUrl: './users.component.scss',
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class UsersComponent {
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
allRoles:Role[] = [];
users: User[] = [];
displayedColumns: string[] = UserColumns.map((col) => col.key)
columnsSchema: any = UserColumns
readonly currentRole = model('');
rolesControl = new FormControl([]);
protected ac = new FormControl('');
constructor(private userService: UserService,
private roleService: RolesService,
private adminService:AdminModuleService) {
this.getAllRoles();
this.getUsers();
}
getAllRoles():any{
this.roleService.getAllRoles().subscribe(
(data : any) => {
this.allRoles = data;
console.log(data);
},
(err : any)=> {
this.allRoles = JSON.parse(err.error).message;
}
);
}
getUsers(){
this.userService.getAdminBoard().subscribe(
(data : any) => {
this.users = data;
console.log(data);
},
(err : any)=> {
console.log(JSON.parse(err.error).message);
}
);
}
filteredRoles(roles : Role[]):any {
return this.allRoles.filter(
(r:Role) => !roles.some((item) => item.id === r.id),
);
}
change($event: Event, roles: Role[]) {
roles.filter(
(r:Role) => !roles.some((item) => item.name?.toLowerCase() === r.name?.toLowerCase()),
)
}
selected(user: User, $event: MatAutocompleteSelectedEvent) {
user.roles.push($event.option.value);
this.saveUserRoles(user);
this.currentRole.set('');
$event.option.deselect();
}
add($event: MatChipInputEvent, user:User) {
/*const value = ($event.value || '').trim();
const role = this.allRoles.find(value1 => value1.name === value);
if(role){
user.roles.push(value);
this.saveUserRoles(user);
}*/
// Clear the input value
this.currentRole.set('');
}
remove(role: any, user: User) {
const updatedRoles: Role[] = user.roles.filter(
(r: Role) => r.id !== role.id // Simple comparison with the role to remove
);
if (updatedRoles && updatedRoles.length >= 1) {
user.roles = updatedRoles;
this.saveUserRoles(user);
}
}
saveUserRoles(user:User){
this.adminService.updateUserRoles(user.id, user).subscribe(
(data:any) => {
console.log(data);
},
(err:any) => {
console.log(err);
}
)
}
}
export const UserColumns = [
{
key: 'username',
type: 'text',
label: 'Name',
required: true,
},
{
key: 'email',
type: 'text',
label: 'Email',
},
{
key: 'roles',
type: 'list',
label: 'Roles',
required: true,
}
];
@@ -80,7 +80,6 @@ export class BoardAdminComponent implements OnInit {
return this.allRoles.filter(
(r:Role) => !roles.some((item) => item.id === r.id),
);
}
// private _filter(value: string): string[] {
@@ -111,7 +110,7 @@ export class BoardAdminComponent implements OnInit {
// this.announcer.announce(`Removed ${fruit}`);
// return [...fruits];
}
}
@@ -27,7 +27,7 @@
</button>
<mat-menu #menu="matMenu">
@if (showAdminBoard) {
<button mat-menu-item routerLink="admin/welcome">
<button mat-menu-item routerLink="admin/admin-welcome">
<mat-icon>admin_panel_settings</mat-icon>
Admin Panel
</button>
@@ -186,7 +186,9 @@ export class MainComponent implements OnInit{
this.refreshToolbar();
dialogLoginRef.close()
this.router.navigate(['main/user/user-welcome']);
this.router.navigate(['main/tutorials']);
},
err => {
dialogLoginRef.componentInstance.openLoginFailedSnackBar(err.error.message);
@@ -0,0 +1,19 @@
<mat-nav-list>
<a mat-list-item routerLink="user-welcome">
<span class="entry">
<mat-icon>house</mat-icon>
@if (!isCollapsed) {
<span >Dashboard</span>
}
</span>
</a>
<a mat-list-item routerLink="tutorials-list">
<span class="entry">
<mat-icon>newspaper</mat-icon>
@if (!isCollapsed) {
<span >Tutorials</span>
}
</span>
</a>
</mat-nav-list>
@@ -0,0 +1,14 @@
.entry{
display: flex;
align-items: center;
gap: 1rem;
padding:0.75rem;
color: rgba(24, 255, 255, 0.96);
}
a.mdc-list-item
{
background-color: rgba(24,255,255,0.04);
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SideBarUserComponent } from './side-bar-user.component';
describe('SideBarUserComponent', () => {
let component: SideBarUserComponent;
let fixture: ComponentFixture<SideBarUserComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SideBarUserComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SideBarUserComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,20 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {MatListItem, MatNavList} from '@angular/material/list';
import {RouterLink} from '@angular/router';
import {MatIcon} from '@angular/material/icon';
@Component({
selector: 'app-side-bar-user',
imports: [
MatIcon,
MatListItem,
MatNavList,
RouterLink
],
templateUrl: './side-bar-user.component.html',
styleUrl: './side-bar-user.component.scss',
schemas:[CUSTOM_ELEMENTS_SCHEMA]
})
export class SideBarUserComponent {
isCollapsed = false;
}
@@ -200,7 +200,7 @@ export const TutorialColumns = [
required: true
},
{
key: 'toBePublished',
key: 'tobepublished',
type: 'boolean',
label: 'To be published',
required: true
@@ -1,34 +1,9 @@
<app-side-bar
<app-side-bar-user
class="pc-sidebar"
>
</app-side-bar>
</app-side-bar-user>
<div class="pc-container">
<router-outlet></router-outlet>
</div>
<!--<mat-toolbar color="primary">-->
<!-- <button mat-icon-button aria-label="Menu icon" (click)="toggleMenu()">-->
<!-- <mat-icon>menu</mat-icon>-->
<!-- </button>-->
<!-- <h1>Responsive Material Sidenavigation</h1>-->
<!--</mat-toolbar>-->
<!--<mat-sidenav-container autosize="true">-->
<!-- <mat-sidenav [ngClass]="!isCollapsed ? 'expanded' : ''" [mode]="isMobile ? 'over' : 'side'" [opened]="isMobile ? 'false' : 'true'">-->
<!-- <mat-nav-list>-->
<!-- <a mat-list-item>-->
<!-- <span class="entry">-->
<!-- <mat-icon>house</mat-icon>-->
<!-- @if (!isCollapsed) {-->
<!-- <span >Dashboard</span>-->
<!-- }-->
<!-- </span>-->
<!-- </a>-->
<!-- </mat-nav-list>-->
<!-- </mat-sidenav>-->
<!-- <mat-sidenav-content>-->
<!-- Content-->
<!-- <router-outlet></router-outlet>-->
<!-- </mat-sidenav-content>-->
<!--</mat-sidenav-container>-->
@@ -1,49 +1,10 @@
mat-toolbar{
position:fixed;
top:0;
z-index: 2;
}
mat-sidenav-container {
height:100%;
}
// Move the content down so that it won't be hidden by the toolbar
mat-sidenav {
padding-top: 3.5rem;
transition: width 0.3s ease;
@media screen and (min-width: 600px) {
padding-top: 4rem;
}
.entry{
display: flex;
align-items: center;
gap: 1rem;
padding:0.75rem;
}
}
// Move the content down so that it won't be hidden by the toolbar
mat-sidenav-content{
padding-top: 3.5rem;
@media screen and (min-width: 600px) {
padding-top: 4rem;
}
}
.expanded {
width: 250px;
}
//---------------
.pc-sidebar{
top: 65px;
overflow-y: auto;
background-color: rgba(153, 153, 153, 0.16);
backdrop-filter: blur(8px);
}
.pc-container{
top: 0px;
@@ -8,6 +8,7 @@ import {MatIcon} from '@angular/material/icon';
import {BreakpointObserver} from '@angular/cdk/layout';
import {MatToolbar} from '@angular/material/toolbar';
import {MatIconButton} from '@angular/material/button';
import {SideBarUserComponent} from '../side-bar-user.component/side-bar-user.component';
@Component({
selector: 'app-user.component',
@@ -21,7 +22,8 @@ import {MatIconButton} from '@angular/material/button';
MatListItem,
MatIcon,
MatToolbar,
MatIconButton
MatIconButton,
SideBarUserComponent
],
templateUrl: './user.component.html',
styleUrl: './user.component.scss',
@@ -1,6 +1,7 @@
package com.jambotronGroup.jambotron.controllers;
import com.jambotronGroup.jambotron.model.Tutorial;
import com.jambotronGroup.jambotron.model.User;
import com.jambotronGroup.jambotron.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
@@ -8,8 +9,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
@CrossOrigin(origins = "${app.origin}", maxAge = 3600, allowCredentials="true")
@RestController
@@ -35,4 +35,31 @@ public class UsersController {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@PutMapping("users/{id}")
public ResponseEntity<?> updateUserRoles(@PathVariable("id") long id, @RequestBody User user) {
Optional<User> userData = userRepository.findById(id);
Map<String, Object> map = new LinkedHashMap<String, Object>();
if (userData.isPresent()) {
User servUser = userData.get();
if(user.getRoles().size() > 0)
servUser.setRoles(user.getRoles());
else {
map.put("status", 0);
map.put("message", "User must have at least one role");
return new ResponseEntity<>(map, HttpStatus.BAD_REQUEST);
}
try {
servUser = userRepository.save(servUser);
} catch (Exception e) {
map.put("status", 0);
map.put("message", e.getMessage());
return new ResponseEntity<>(map,HttpStatus.INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<>(servUser, HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
}