diff --git a/src/app/graph/components/data-analysis/data-analysis.component.html b/src/app/graph/components/data-analysis/data-analysis.component.html index a401e82..04b207e 100644 --- a/src/app/graph/components/data-analysis/data-analysis.component.html +++ b/src/app/graph/components/data-analysis/data-analysis.component.html @@ -40,25 +40,25 @@
@for (account of accounts; track $index) { -
- {{ account.entityName }} -
- - - info - -
+ }
- +
diff --git a/src/app/graph/components/data-analysis/data-analysis.component.ts b/src/app/graph/components/data-analysis/data-analysis.component.ts index b9ec158..2fdcdd0 100644 --- a/src/app/graph/components/data-analysis/data-analysis.component.ts +++ b/src/app/graph/components/data-analysis/data-analysis.component.ts @@ -69,7 +69,7 @@ export class DataAnalysisComponent implements AfterViewInit { private loadGraphService: LoadGraphService, private dialog: MatDialog, private changeDetector: ChangeDetectorRef, - private loadingService: LoadingService, + private loadingService: LoadingService ) {} handlePageEvent(e: PageEvent) { @@ -109,7 +109,7 @@ export class DataAnalysisComponent implements AfterViewInit { this.networkInstance = new Network( this.el.nativeElement, this.data, - getOptions(), + getOptions() ); // Listen for the context menu event (right-click) @@ -127,7 +127,7 @@ export class DataAnalysisComponent implements AfterViewInit { this.changeDetector.detectChanges(); const rightClickNodeInfoElem = document.getElementById( - 'right-click-node-info', + 'right-click-node-info' ) as HTMLElement; rightClickNodeInfoElem.dataset['nodeid'] = nodeId.toString(); @@ -208,17 +208,15 @@ export class DataAnalysisComponent implements AfterViewInit { console.log('edge click: ', edgeId); } - getInfo( - account: { id: number; entityName: string } = { id: 0, entityName: 'test' }, - ) { + getInfo(account?: number) { // todo: fix this - // if (!account) { - // account = ( - // document.getElementById('right-click-node-info') as HTMLElement - // ).dataset['nodeid']; - // } + if (!account) { + account = ( + document.getElementById('right-click-node-info') as HTMLElement + ).dataset['nodeid'] as unknown as number; + } - this.loadGraphService.getNodeInfo(account.id).subscribe({ + this.loadGraphService.getNodeInfo(account).subscribe({ next: (data) => { this.dialog.open(InfoDialogComponent, { width: '105rem', @@ -293,7 +291,7 @@ export class DataAnalysisComponent implements AfterViewInit { const dialogRef = this.dialog.open( ColorPickerDialogComponent, - dialogConfig, + dialogConfig ); dialogRef.afterClosed().subscribe((result) => { diff --git a/src/app/user/components/dashboard/manage-account/manage-account.component.html b/src/app/user/components/dashboard/manage-account/manage-account.component.html index 7dfb28b..fc8ca43 100644 --- a/src/app/user/components/dashboard/manage-account/manage-account.component.html +++ b/src/app/user/components/dashboard/manage-account/manage-account.component.html @@ -1,74 +1,76 @@ -
- -
- - First Name - - - - Last Name - - - - Email - - - - Phone Number - - -
- - -
-
-
- + +
+ +
+ + First Name + + + + Last Name + + + + Email + + + + Phone Number + + +
+ + +
+
+
+ +
diff --git a/src/app/user/components/dashboard/manage-account/manage-account.component.scss b/src/app/user/components/dashboard/manage-account/manage-account.component.scss index 2963768..aca6962 100644 --- a/src/app/user/components/dashboard/manage-account/manage-account.component.scss +++ b/src/app/user/components/dashboard/manage-account/manage-account.component.scss @@ -1,28 +1,34 @@ -.add-user-form { +.container { display: flex; flex-direction: column; gap: 1rem; -} -.container { - width: 100%; - display: flex; - gap: 1rem; + .add-user-form { + display: flex; + flex-direction: column; + gap: 1rem; + } + + .info-container { + width: 100%; + display: flex; + gap: 1rem; - .form { - width: 50%; + .form { + width: 50%; - .btn_container { - display: flex; - gap: 1rem; + .btn_container { + display: flex; + gap: 1rem; - .btn { - width: 25%; + .btn { + width: 25%; + } } } - } - .validation { - width: 50%; + .validation { + width: 50%; + } } } diff --git a/src/app/user/components/dashboard/manage-account/manage-account.component.spec.ts b/src/app/user/components/dashboard/manage-account/manage-account.component.spec.ts index 054c822..351299e 100644 --- a/src/app/user/components/dashboard/manage-account/manage-account.component.spec.ts +++ b/src/app/user/components/dashboard/manage-account/manage-account.component.spec.ts @@ -1,47 +1,246 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { ManageAccountComponent } from './manage-account.component'; -import { DashboardHeaderComponent } from '../../../../shared/components/dashboard-header/dashboard-header.component'; -import { CardComponent } from '../../../../shared/components/card/card.component'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; -import { RouterModule } from '@angular/router'; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { ValidationStatusComponent } from '../../../../shared/components/validation-status/validation-status.component'; +import { SharedModule } from '../../../../shared/shared.module'; +import { UserInformation } from '../../../models/ManageUsers'; +import { of, throwError } from 'rxjs'; +import { UserService } from '../../../services/user/user.service'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { LoadingService } from '../../../../shared/services/loading.service'; +import { DangerSuccessNotificationComponent } from '../../../../shared/components/danger-success-notification/danger-success-notification.component'; +import { ProfileHeaderComponent } from './profile-header/profile-header.component'; +import { MatIconModule } from '@angular/material/icon'; describe('ManageAccountComponent', () => { let component: ManageAccountComponent; let fixture: ComponentFixture; + let userServiceSpy: jasmine.SpyObj; + let snackBarSpy: jasmine.SpyObj; + let loadingServiceSpy: jasmine.SpyObj; beforeEach(async () => { + const userServiceMock = jasmine.createSpyObj('UserService', [ + 'updateUser', + 'getLoginUserInfo', + ]); + const snackBarMock = jasmine.createSpyObj('MatSnackBar', [ + 'openFromComponent', + ]); + const loadingServiceMock = jasmine.createSpyObj('LoadingService', [ + 'setLoading', + ]); + await TestBed.configureTestingModule({ - declarations: [ - ManageAccountComponent, - DashboardHeaderComponent, - CardComponent, - ValidationStatusComponent, - ], + declarations: [ManageAccountComponent, ProfileHeaderComponent], imports: [ + SharedModule, MatIconModule, + ReactiveFormsModule, + FormsModule, MatFormFieldModule, MatInputModule, - RouterModule.forRoot([]), + MatButtonModule, BrowserAnimationsModule, - ReactiveFormsModule, ], - providers: [provideHttpClient(), provideHttpClientTesting()], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + { provide: UserService, useValue: userServiceMock }, + { provide: MatSnackBar, useValue: snackBarMock }, + { provide: LoadingService, useValue: loadingServiceMock }, + ], }).compileComponents(); + userServiceSpy = TestBed.inject(UserService) as jasmine.SpyObj; + snackBarSpy = TestBed.inject(MatSnackBar) as jasmine.SpyObj; + loadingServiceSpy = TestBed.inject( + LoadingService, + ) as jasmine.SpyObj; + }); + + beforeEach(() => { fixture = TestBed.createComponent(ManageAccountComponent); component = fixture.componentInstance; + const mockUserInfo: UserInformation = { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + phoneNumber: '09123456789', + image: 'image.jpg', + }; + + userServiceSpy.getLoginUserInfo.and.returnValue(of(mockUserInfo)); fixture.detectChanges(); }); - it('should create', () => { + it('should create the component', () => { expect(component).toBeTruthy(); }); + + it('should populate the form with user information on init', () => { + const mockUserInfo: UserInformation = { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + phoneNumber: '09123456789', + image: 'image.jpg', + }; + + userServiceSpy.getLoginUserInfo.and.returnValue(of(mockUserInfo)); + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.userInfo).toEqual(mockUserInfo); + + expect(component.myForm.get('firstName')?.value).toBe('John'); + expect(component.myForm.get('lastName')?.value).toBe('Doe'); + expect(component.myForm.get('email')?.value).toBe('john.doe@example.com'); + expect(component.myForm.get('phoneNumber')?.value).toBe('09123456789'); + }); + + it('should create a form with controls', () => { + expect(component.myForm.contains('firstName')).toBeTruthy(); + expect(component.myForm.contains('lastName')).toBeTruthy(); + expect(component.myForm.contains('email')).toBeTruthy(); + expect(component.myForm.contains('phoneNumber')).toBeTruthy(); + }); + + it('should make the email control required', () => { + const emailControl = component.myForm.get('email'); + emailControl?.setValue(''); + expect(emailControl?.valid).toBeFalsy(); + }); + + it('should submit the form when valid', () => { + spyOn(component, 'onSubmit'); + + component.myForm.get('firstName')?.setValue('John'); + component.myForm.get('lastName')?.setValue('Doe'); + component.myForm.get('email')?.setValue('john.doe@example.com'); + component.myForm.get('phoneNumber')?.setValue('1234567890'); + + const form = fixture.nativeElement.querySelector('form'); + form.dispatchEvent(new Event('submit')); + + expect(component.onSubmit).toHaveBeenCalled(); + }); + + it('should reset the form when the reset button is clicked', () => { + component.userInfo = { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + phoneNumber: '09123456789', + image: 'image.jpg', + }; + + component.myForm.patchValue({ + firstName: 'Jane', + lastName: 'Smith', + email: 'jane.smith@example.com', + phoneNumber: '09876543210', + }); + + component.resetUserInfo(); + + expect(component.myForm.get('firstName')?.value).toBe( + component.userInfo.firstName, + ); + expect(component.myForm.get('lastName')?.value).toBe( + component.userInfo.lastName, + ); + expect(component.myForm.get('email')?.value).toBe(component.userInfo.email); + expect(component.myForm.get('phoneNumber')?.value).toBe( + component.userInfo.phoneNumber, + ); + }); + + it('should set focusedField when input is focused', () => { + const firstNameInput = fixture.nativeElement.querySelector( + 'input[name="firstName"]', + ); + firstNameInput.dispatchEvent(new Event('focus')); + + expect(component.focusedField).toBe('firstName'); + }); + + it('should call updateUser and show success notification when form is valid and update is successful', () => { + // Arrange + component.myForm.patchValue({ + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + phoneNumber: '09123456789', + }); + component.myForm.markAsDirty(); + userServiceSpy.updateUser.and.returnValue(of({})); + + // Act + component.onSubmit(); + + // Assert + expect(userServiceSpy.updateUser).toHaveBeenCalledWith( + component.myForm.value, + ); + expect(snackBarSpy.openFromComponent).toHaveBeenCalledWith( + DangerSuccessNotificationComponent, + { + data: 'User information updated successfully!', + panelClass: ['notification-class-success'], + duration: 2000, + }, + ); + expect(loadingServiceSpy.setLoading).toHaveBeenCalledWith(false); + expect(component.userInfo).toEqual(component.myForm.value); + }); + + it('should show error notification when updateUser fails', () => { + component.myForm.patchValue({ + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + phoneNumber: '09123456789', + }); + component.myForm.markAsDirty(); + + const mockError = { error: { message: 'Update failed' } }; + userServiceSpy.updateUser.and.returnValue(throwError(() => mockError)); + + component.onSubmit(); + + expect(userServiceSpy.updateUser).toHaveBeenCalledWith( + component.myForm.value, + ); + expect(snackBarSpy.openFromComponent).toHaveBeenCalledWith( + DangerSuccessNotificationComponent, + { + data: 'Update failed', + panelClass: ['notification-class-danger'], + duration: 2000, + }, + ); + expect(loadingServiceSpy.setLoading).toHaveBeenCalledWith(false); + }); + + it('should not call updateUser if form is invalid', () => { + // Arrange + component.myForm.patchValue({ + firstName: '', + lastName: 'Doe', + email: 'john.doe@example.com', + phoneNumber: '09123456789', + }); + + // Act + component.onSubmit(); + + // Assert + expect(userServiceSpy.updateUser).not.toHaveBeenCalled(); + }); }); diff --git a/src/app/user/components/dashboard/manage-account/profile-header/profile-header.component.html b/src/app/user/components/dashboard/manage-account/profile-header/profile-header.component.html new file mode 100644 index 0000000..b8cd91b --- /dev/null +++ b/src/app/user/components/dashboard/manage-account/profile-header/profile-header.component.html @@ -0,0 +1,33 @@ + +
+
+
+ Profile Picture + + +
+
+
+
+

{{ userInfo.firstName }} {{ userInfo.lastName }}

+

+ email + {{ userInfo.email }} +

+

+ phone + {{ userInfo.phoneNumber }} +

+
+
diff --git a/src/app/user/components/dashboard/manage-account/profile-header/profile-header.component.scss b/src/app/user/components/dashboard/manage-account/profile-header/profile-header.component.scss new file mode 100644 index 0000000..7749136 --- /dev/null +++ b/src/app/user/components/dashboard/manage-account/profile-header/profile-header.component.scss @@ -0,0 +1,90 @@ +.container { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.header { + position: relative; + background-image: url("https://i.postimg.cc/J4fR2XfB/header-bg.jpg"); + background-size: cover; + background-position: center; + height: 19rem; + width: 100%; + border-radius: 0.5rem; +} + +.profile-info { + display: flex; + align-items: center; + position: absolute; + bottom: -120px; + left: 30px; +} + +.profile-pic { + width: 15.5rem; + height: 15.5rem; + border-radius: 50%; + overflow: hidden; + border: 0.3rem solid var(--card-background-color); +} + +.edit-btn { + top: -40px; + left: 100px; + position: relative; + color: var(--mat-app-text-color); + background-color: var(--mat-app-background-color); + border-radius: 50%; + padding: 5px 5.75px 0 5.75px; +} + +.profile-pic img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.profile-details { + margin-left: 20px; + display: flex; + flex-direction: column; + padding-left: 270px; + gap: 0.5rem; + + > p { + display: flex; + gap: 0.5rem; + } +} + +.profile-details h1 { + font-size: 1.6rem; +} + +.profile-actions { + position: absolute; + top: 20px; + right: 30px; +} + +/* Adjust for mobile */ +@media (max-width: 600px) { + .profile-info { + flex-direction: column; + align-items: center; + bottom: -70px; + left: 20px; + } + + .profile-details h1 { + font-size: 1.25rem; + text-align: center; + } + + .profile-actions { + top: 10px; + right: 20px; + } +} diff --git a/src/app/user/components/dashboard/manage-account/profile-header/profile-header.component.spec.ts b/src/app/user/components/dashboard/manage-account/profile-header/profile-header.component.spec.ts new file mode 100644 index 0000000..d9c1031 --- /dev/null +++ b/src/app/user/components/dashboard/manage-account/profile-header/profile-header.component.spec.ts @@ -0,0 +1,37 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProfileHeaderComponent } from './profile-header.component'; +import { CardComponent } from '../../../../../shared/components/card/card.component'; +import { MatIconModule } from '@angular/material/icon'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; + +describe('ProfileHeaderComponent', () => { + let component: ProfileHeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ProfileHeaderComponent, CardComponent], + imports: [MatIconModule], + providers: [provideHttpClient(), provideHttpClientTesting()], + }).compileComponents(); + + fixture = TestBed.createComponent(ProfileHeaderComponent); + component = fixture.componentInstance; + + component.userInfo = { + firstName: 'Kevin', + lastName: 'Smith', + email: 'kevin.smith@example.com', + phoneNumber: '+123456789', + image: 'image.jpg', + }; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/user/components/dashboard/manage-account/profile-header/profile-header.component.ts b/src/app/user/components/dashboard/manage-account/profile-header/profile-header.component.ts new file mode 100644 index 0000000..95780a0 --- /dev/null +++ b/src/app/user/components/dashboard/manage-account/profile-header/profile-header.component.ts @@ -0,0 +1,54 @@ +import { Component, Input } from '@angular/core'; +import { UserInformation } from '../../../../models/ManageUsers'; +import { UserService } from '../../../../services/user/user.service'; +import { DangerSuccessNotificationComponent } from '../../../../../shared/components/danger-success-notification/danger-success-notification.component'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { LoadingService } from '../../../../../shared/services/loading.service'; + +@Component({ + selector: 'app-profile-header', + templateUrl: './profile-header.component.html', + styleUrl: './profile-header.component.scss', +}) +export class ProfileHeaderComponent { + @Input({ required: true }) userInfo!: UserInformation; + + constructor( + private userService: UserService, + private _snackBar: MatSnackBar, + private loadingService: LoadingService, + ) {} + + onFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + const file = input.files[0]; + + console.log('Selected file:', file); + this.userService.uploadImage(file).subscribe({ + next: () => { + this._snackBar.openFromComponent(DangerSuccessNotificationComponent, { + data: 'User profile image updated successfully!', + panelClass: ['notification-class-success'], + duration: 2000, + }); + this.loadingService.setLoading(false); + this.userService + .getLoginUserInfo() + .subscribe((data: UserInformation) => { + this.userInfo = data; + console.log(11, this.userInfo); + }); + }, + error: (error) => { + this._snackBar.openFromComponent(DangerSuccessNotificationComponent, { + data: error.error.message, + panelClass: ['notification-class-danger'], + duration: 2000, + }); + this.loadingService.setLoading(false); + }, + }); + } + } +} diff --git a/src/app/user/components/dashboard/manage-users/add-user/add-user.component.html b/src/app/user/components/dashboard/manage-users/add-user/add-user.component.html index 1c63cd7..e01706f 100644 --- a/src/app/user/components/dashboard/manage-users/add-user/add-user.component.html +++ b/src/app/user/components/dashboard/manage-users/add-user/add-user.component.html @@ -31,8 +31,8 @@

Add User

diff --git a/src/app/user/components/dashboard/manage-users/add-user/add-user.component.spec.ts b/src/app/user/components/dashboard/manage-users/add-user/add-user.component.spec.ts index 4b605d7..37999d7 100644 --- a/src/app/user/components/dashboard/manage-users/add-user/add-user.component.spec.ts +++ b/src/app/user/components/dashboard/manage-users/add-user/add-user.component.spec.ts @@ -55,7 +55,7 @@ describe('AddUserComponent', () => { component.myForm = new FormGroup({ firstName: new FormControl(''), lastName: new FormControl(''), - username: new FormControl(''), + userName: new FormControl(''), password: new FormControl(''), confirmPassword: new FormControl(''), email: new FormControl(''), @@ -73,7 +73,7 @@ describe('AddUserComponent', () => { it('should initialize the form with controls', () => { expect(component.myForm.contains('firstName')).toBeTruthy(); expect(component.myForm.contains('lastName')).toBeTruthy(); - expect(component.myForm.contains('username')).toBeTruthy(); + expect(component.myForm.contains('userName')).toBeTruthy(); expect(component.myForm.contains('password')).toBeTruthy(); expect(component.myForm.contains('confirmPassword')).toBeTruthy(); expect(component.myForm.contains('email')).toBeTruthy(); @@ -87,7 +87,7 @@ describe('AddUserComponent', () => { component.myForm.setValue({ firstName: 'John', lastName: 'Doe', - username: 'johndoe', + userName: 'johndoe', password: 'passwordAa@12', confirmPassword: 'passwordAa@12', email: 'john.doe@example.com', diff --git a/src/app/user/components/dashboard/manage-users/add-user/add-user.component.ts b/src/app/user/components/dashboard/manage-users/add-user/add-user.component.ts index ff9af95..926dda0 100644 --- a/src/app/user/components/dashboard/manage-users/add-user/add-user.component.ts +++ b/src/app/user/components/dashboard/manage-users/add-user/add-user.component.ts @@ -37,7 +37,7 @@ export class AddUserComponent implements OnInit { this.myForm = new FormGroup({ firstName: new FormControl('', Validators.required), lastName: new FormControl('', Validators.required), - username: new FormControl('', Validators.required), + userName: new FormControl('', Validators.required), password: new FormControl('', [ Validators.required, Validators.pattern( diff --git a/src/app/user/components/dashboard/manage-users/edit-user/edit-user.component.html b/src/app/user/components/dashboard/manage-users/edit-user/edit-user.component.html index d13492a..d0addd2 100644 --- a/src/app/user/components/dashboard/manage-users/edit-user/edit-user.component.html +++ b/src/app/user/components/dashboard/manage-users/edit-user/edit-user.component.html @@ -31,8 +31,8 @@

Edit User

@@ -55,13 +55,11 @@

Edit User

class="radio-buttons" formControlName="roleName" > - Admin - Data Manager - Data Analyst + @for (role of roles; track role.id) { + {{ role.name.replace("-", " ") }} + + }