diff --git a/web/ui/dashboard/src/app/models/group.model.ts b/web/ui/dashboard/src/app/models/group.model.ts index 513fe64ee8..a09f4140e3 100644 --- a/web/ui/dashboard/src/app/models/group.model.ts +++ b/web/ui/dashboard/src/app/models/group.model.ts @@ -29,6 +29,7 @@ export interface GROUP { created_at: Date; updated_at: Date; type: 'incoming' | 'outgoing'; + selected?: boolean; } export interface SOURCE { diff --git a/web/ui/dashboard/src/app/models/teams.model.ts b/web/ui/dashboard/src/app/models/teams.model.ts new file mode 100644 index 0000000000..5fd67d807c --- /dev/null +++ b/web/ui/dashboard/src/app/models/teams.model.ts @@ -0,0 +1,14 @@ +export interface TEAMS { + role: { + groups: string[]; + type: string; + }; + uid: string; + status?: boolean; + invitee_email?: string; + user_metadata: { + first_name: string; + last_name: string; + email: string; + }; +} diff --git a/web/ui/dashboard/src/app/private/components/table-loader/table-loader.component.html b/web/ui/dashboard/src/app/private/components/table-loader/table-loader.component.html index b4373ca817..168285a57a 100644 --- a/web/ui/dashboard/src/app/private/components/table-loader/table-loader.component.html +++ b/web/ui/dashboard/src/app/private/components/table-loader/table-loader.component.html @@ -1,102 +1,102 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
{{ head }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ head }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/web/ui/dashboard/src/app/private/components/table-loader/table-loader.component.ts b/web/ui/dashboard/src/app/private/components/table-loader/table-loader.component.ts index 6beb52d3b3..fc1259080b 100644 --- a/web/ui/dashboard/src/app/private/components/table-loader/table-loader.component.ts +++ b/web/ui/dashboard/src/app/private/components/table-loader/table-loader.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnInit } from '@angular/core'; }) export class TableLoaderComponent implements OnInit { @Input() tableHead!: string[]; - + @Input() tableClass!: string; constructor() {} ngOnInit(): void {} diff --git a/web/ui/dashboard/src/app/private/pages/projects/projects.component.html b/web/ui/dashboard/src/app/private/pages/projects/projects.component.html index a9c43ae2ef..031945ec10 100644 --- a/web/ui/dashboard/src/app/private/pages/projects/projects.component.html +++ b/web/ui/dashboard/src/app/private/pages/projects/projects.component.html @@ -1,6 +1,6 @@ - +
@@ -99,4 +99,4 @@

You have no project yet

All your project's summary at a glance

-
\ No newline at end of file +
diff --git a/web/ui/dashboard/src/app/private/pages/teams/teams.component.html b/web/ui/dashboard/src/app/private/pages/teams/teams.component.html new file mode 100644 index 0000000000..a074d273ea --- /dev/null +++ b/web/ui/dashboard/src/app/private/pages/teams/teams.component.html @@ -0,0 +1,243 @@ +
+
+
+

Teams

+

View and manage your team members.

+
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
{{ head }}
+
+
{{ team?.user_metadata?.first_name?.slice(0, 1) }}{{ team?.user_metadata?.last_name?.slice(0, 1) }}
+
{{ team.user_metadata.first_name || '-' }} {{ team.user_metadata.last_name || '-' }}
+
+
+
{{ team.role.type === 'super_user' ? 'Super user' : team.role.type }}
+
+
All projects
+
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + +
{{ selectedFilterOption === 'pending' && head === 'Name' ? 'Email' : head }}
+
{{ team.invitee_email }}
+
+
{{ team.role.type === 'super_user' ? 'Super user' : team.role.type }}
+
+
All projects
+
+
+ + +
+
+
+ +
+
+
+ add team empty +

+ You have no {{ searchString ? 'one with the name ' + searchString + ' on your team' : selectedFilterOption === 'active' ? 'one on your team yet' : 'pending invites' }} +

+

You can invite team members to join your organization and assign them roles to projects

+ +
+
+
+ +
+ +
+ + + + + + diff --git a/web/ui/dashboard/src/app/private/pages/teams/teams.component.scss b/web/ui/dashboard/src/app/private/pages/teams/teams.component.scss new file mode 100644 index 0000000000..e78b6816b0 --- /dev/null +++ b/web/ui/dashboard/src/app/private/pages/teams/teams.component.scss @@ -0,0 +1,12 @@ + +.modal--body { + min-height: 100%; +} +.empty-state-img { + height: 110px; + width: 100px; + &.big { + height: 130px; + width: 120px; + } +} diff --git a/web/ui/dashboard/src/app/private/pages/teams/teams.component.spec.ts b/web/ui/dashboard/src/app/private/pages/teams/teams.component.spec.ts new file mode 100644 index 0000000000..b307ef4dfb --- /dev/null +++ b/web/ui/dashboard/src/app/private/pages/teams/teams.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TeamsComponent } from './teams.component'; + +describe('TeamsComponent', () => { + let component: TeamsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TeamsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TeamsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/web/ui/dashboard/src/app/private/pages/teams/teams.component.ts b/web/ui/dashboard/src/app/private/pages/teams/teams.component.ts new file mode 100644 index 0000000000..9cee0c2d76 --- /dev/null +++ b/web/ui/dashboard/src/app/private/pages/teams/teams.component.ts @@ -0,0 +1,132 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { PAGINATION } from 'src/app/models/global.model'; +import { TEAMS } from 'src/app/models/teams.model'; +import { GeneralService } from 'src/app/services/general/general.service'; +import { TeamsService } from './teams.service'; + +@Component({ + selector: 'app-teams', + templateUrl: './teams.component.html', + styleUrls: ['./teams.component.scss'] +}) +export class TeamsComponent implements OnInit { + tableHead: string[] = ['Name', 'Role', 'Projects', '']; + filterOptions: ['active', 'pending'] = ['active', 'pending']; + showInviteTeamMemberModal = this.router.url.split('/')[2]?.includes('new'); + showTeamMemberDropdown = false; + showTeamGroupDropdown = false; + showSuccessModal = false; + showDeactivateModal = false; + selectedMember!: TEAMS; + isFetchingTeamMembers = false; + isFetchingPendingInvites = false; + deactivatingUser = false; + searchString!: string; + organisationId!: string; + teams!: { pagination: PAGINATION; content: TEAMS[] }; + pendingInvites!: { pagination: PAGINATION; content: TEAMS[] }; + currentId!: string; + selectedFilterOption: 'active' | 'pending' = 'active'; + showOverlay = false; + noData = false; + noInvitesData = false; + showFilterDropdown = false; + invitingUser = false; + inviteUserForm: FormGroup = this.formBuilder.group({ + invitee_email: ['', Validators.compose([Validators.required, Validators.email])], + role: this.formBuilder.group({ + type: ['super_user', Validators.required] + }) + }); + + constructor(private generalService: GeneralService, private router: Router, private route: ActivatedRoute, private teamService: TeamsService, private formBuilder:FormBuilder) {} + + ngOnInit() { + this.toggleFilter(this.route.snapshot.queryParams?.inviteType ?? 'active'); + } + + async fetchTeamMembers(requestDetails?: { searchString?: string; page?: number }) { + this.isFetchingTeamMembers = true; + const page = requestDetails?.page || this.route.snapshot.queryParams.page || 1; + try { + const response = await this.teamService.getTeamMembers({ pageNo: page, searchString: requestDetails?.searchString }); + this.teams = response.data; + response.data.content.length === 0 ? (this.noData = true) : (this.noData = false); + + this.isFetchingTeamMembers = false; + } catch { + this.isFetchingTeamMembers = false; + } + } + + toggleFilter(selectedFilter: 'active' | 'pending') { + this.selectedFilterOption = selectedFilter; + this.selectedFilterOption === 'active' ? this.fetchTeamMembers() : this.fetchPendingTeamMembers(); + if(!this.router.url.split('/')[2]) this.addFilterToUrl(); + } + async fetchPendingTeamMembers(requestDetails?: { page?: number }) { + this.isFetchingPendingInvites = true; + const page = requestDetails?.page || this.route.snapshot.queryParams.pendingInvites || 1; + try { + const response = await this.teamService.getPendingTeamMembers({ pageNo: page }); + this.pendingInvites = response.data; + response.data.content.length === 0 ? (this.noInvitesData = true) : (this.noInvitesData = false); + this.isFetchingPendingInvites = false; + } catch { + this.isFetchingPendingInvites = false; + } + } + + searchTeam(searchDetails: { searchInput?: any }) { + const searchString: string = searchDetails?.searchInput?.target?.value || this.searchString; + this.fetchTeamMembers({ searchString: searchString }); + } + + async deactivateMember() { + this.deactivatingUser = true; + const requestOptions = { + memberId: this.selectedMember?.uid + }; + try { + const response = await this.teamService.deactivateTeamMember(requestOptions); + if (response.status) this.showDeactivateModal = false; + this.generalService.showNotification({ style: 'success', message: response.message }); + this.fetchTeamMembers(); + this.deactivatingUser = false; + } catch { + this.deactivatingUser = false; + } + } + + showDropdown(id: string) { + this.showOverlay = false; + this.currentId == id ? (this.currentId = '') : (this.currentId = id); + } + + addFilterToUrl() { + const queryParams: any = {}; + queryParams.inviteType = this.selectedFilterOption; + this.router.navigate([], { queryParams: Object.assign({}, queryParams) }); + } + + async inviteUser() { + if (this.inviteUserForm.invalid) { + (this.inviteUserForm).values(this.inviteUserForm.controls).forEach((control: FormControl) => { + control?.markAsTouched(); + }); + return; + } + this.invitingUser = true; + try { + const response = await this.teamService.inviteUserToOrganisation(this.inviteUserForm.value); + this.generalService.showNotification({ message: response.message, style: 'success' }); + this.inviteUserForm.reset(); + this.invitingUser = false; + this.router.navigate(['/team'], { queryParams: { inviteType: 'pending' } }); + } catch { + this.invitingUser = false; + } + } +} diff --git a/web/ui/dashboard/src/app/private/pages/teams/teams.module.ts b/web/ui/dashboard/src/app/private/pages/teams/teams.module.ts new file mode 100644 index 0000000000..a6821ed2be --- /dev/null +++ b/web/ui/dashboard/src/app/private/pages/teams/teams.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TeamsComponent } from './teams.component'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; +import { TableLoaderModule } from '../../components/table-loader/table-loader.module'; + +const routes: Routes = [ + { path: '', component: TeamsComponent }, + { path: 'new', component: TeamsComponent } +]; + +@NgModule({ + declarations: [TeamsComponent], + imports: [CommonModule, FormsModule, TableLoaderModule, ReactiveFormsModule, RouterModule.forChild(routes)] +}) +export class TeamsModule {} diff --git a/web/ui/dashboard/src/app/private/pages/teams/teams.service.spec.ts b/web/ui/dashboard/src/app/private/pages/teams/teams.service.spec.ts new file mode 100644 index 0000000000..6c532974e6 --- /dev/null +++ b/web/ui/dashboard/src/app/private/pages/teams/teams.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { TeamsService } from './teams.service'; + +describe('TeamsService', () => { + let service: TeamsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(TeamsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/web/ui/dashboard/src/app/private/pages/teams/teams.service.ts b/web/ui/dashboard/src/app/private/pages/teams/teams.service.ts new file mode 100644 index 0000000000..e831ca6b08 --- /dev/null +++ b/web/ui/dashboard/src/app/private/pages/teams/teams.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@angular/core'; +import { HTTP_RESPONSE } from 'src/app/models/http.model'; +import { HttpService } from 'src/app/services/http/http.service'; +import { PrivateService } from '../../private.service'; + +@Injectable({ + providedIn: 'root' +}) +export class TeamsService { + constructor(private http: HttpService, private privateService: PrivateService) {} + + async getTeamMembers(requestDetails: { searchString?: string; pageNo?: number }): Promise { + try { + const response = await this.http.request({ + url: `${this.privateService.urlFactory('org')}/members?sort=AESC&page=${requestDetails?.pageNo || 1}&perPage=20${requestDetails?.searchString ? `&q=${requestDetails?.searchString}` : ''}`, + method: 'get' + }); + return response; + } catch (error: any) { + return error; + } + } + + async getPendingTeamMembers(requestDetails: { pageNo?: number }): Promise { + try { + const response = await this.http.request({ + url: `${this.privateService.urlFactory('org')}/pending_invites?sort=AESC&page=${requestDetails?.pageNo || 1}&perPage=20`, + method: 'get' + }); + return response; + } catch (error: any) { + return error; + } + } + + async inviteUserToOrganisation(requestDetails: { firstname: string; lastname: string; email: string; role: string; groups: string[] }): Promise { + try { + const response = await this.http.request({ + url: `${this.privateService.urlFactory('org')}/invite_user`, + body: requestDetails, + method: 'post' + }); + return response; + } catch (error: any) { + return error; + } + } + + async deactivateTeamMember(requestOptions: { memberId: string }) { + try { + const response = await this.http.request({ + url: `${this.privateService.urlFactory('org')}/members/${requestOptions.memberId}`, + method: 'delete' + }); + return response; + } catch (error: any) { + return error; + } + } +} diff --git a/web/ui/dashboard/src/app/private/private-routing.module.ts b/web/ui/dashboard/src/app/private/private-routing.module.ts index f509ecf3e0..443ece1a00 100644 --- a/web/ui/dashboard/src/app/private/private-routing.module.ts +++ b/web/ui/dashboard/src/app/private/private-routing.module.ts @@ -28,6 +28,10 @@ const routes: Routes = [ path: 'app-portal/:token', loadChildren: () => import('./pages/app/app.module').then(m => m.AppModule) }, + { + path: 'team', + loadChildren: () => import('./pages/teams/teams.module').then(m => m.TeamsModule) + }, { path: 'organisation-settings', loadChildren: () => import('./pages/organisation/organisation.module').then(m => m.OrganisationModule) diff --git a/web/ui/dashboard/src/app/private/private.component.html b/web/ui/dashboard/src/app/private/private.component.html index 7f002e55ef..c48616efa0 100644 --- a/web/ui/dashboard/src/app/private/private.component.html +++ b/web/ui/dashboard/src/app/private/private.component.html @@ -8,16 +8,13 @@ @@ -29,7 +26,7 @@ -