diff --git a/src/ipa-bcfier-ui/src/app/components/bcf-file/bcf-file.component.html b/src/ipa-bcfier-ui/src/app/components/bcf-file/bcf-file.component.html index 6bd76550..0cffdf7e 100644 --- a/src/ipa-bcfier-ui/src/app/components/bcf-file/bcf-file.component.html +++ b/src/ipa-bcfier-ui/src/app/components/bcf-file/bcf-file.component.html @@ -1,65 +1,90 @@
-
- - -
-
- - Search - - @if (search) { - - } - -
-
- - - - - {{ topic.title }} - {{ - topic.creationDate | date : "dd.MM.yyyy" - }} - - - - - -
- chat - {{ topic.comments.length }} + + +
+ +
+
+ +
+
+ +
-
- visibility - {{ topic.viewpoints.length }} + +
+ + Search + + @if (search) { + + } + + filter_list
- - -
+
+ +
+ + + + + {{ topic.title }} + {{ + topic.creationDate | date : "dd.MM.yyyy" + }} + + + + + +
+ chat + {{ topic.comments.length }} +
+
+ visibility + {{ topic.viewpoints.length }} +
+
+
+
+
+
): void { + const { status, type, users, issueRange } = filters.value; + if ( + status === undefined || + type === undefined || + users === undefined || + issueRange === undefined || + issueRange?.start === undefined || + issueRange?.end === undefined + ) { + return; + } + + const isValuePresentInFilters = + !!status || !!type || !!users || !!issueRange.start || !!issueRange.end; + + this.filtredTopics = isValuePresentInFilters + ? [ + ...this.issueFilterService.filterIssue( + this.bcfFile.topics, + status, + type, + users, + issueRange?.start, + issueRange?.end + ), + ] + : this.bcfFile.topics; + } } diff --git a/src/ipa-bcfier-ui/src/app/components/issue-filters/issue-filters.component.html b/src/ipa-bcfier-ui/src/app/components/issue-filters/issue-filters.component.html new file mode 100644 index 00000000..a8b2b259 --- /dev/null +++ b/src/ipa-bcfier-ui/src/app/components/issue-filters/issue-filters.component.html @@ -0,0 +1,57 @@ +
+
+ + Status + + @for (status of (issueStatuses$ | async)?.values(); track status) { + {{ status }} + } + + + + Type + + @for (type of (issueTypes$ | async)?.values(); track type) { + {{ type }} + } + + + + Responsible + + @for (user of (users$ | async); track user) { + {{ user }} + } + + + + Due Date + + + + + MM/DD/YYYY – MM/DD/YYYY + + + + @if (filtersForm.get('issueRange.start')?.hasError('matStartDateInvalid')) + { + Invalid start date + } @if (filtersForm.get('issueRange.end')?.hasError('matEndDateInvalid')) { + Invalid end date + } + +
+ + +
diff --git a/src/ipa-bcfier-ui/src/app/components/issue-filters/issue-filters.component.scss b/src/ipa-bcfier-ui/src/app/components/issue-filters/issue-filters.component.scss new file mode 100644 index 00000000..fd98ff39 --- /dev/null +++ b/src/ipa-bcfier-ui/src/app/components/issue-filters/issue-filters.component.scss @@ -0,0 +1,17 @@ +.container { + display: flex; + justify-content: space-between; + flex-direction: column; + height: 99%; + &_filters { + flex: 1; + } +} + +mat-form-field { + width: 100%; +} + +button { + margin: 5px; +} diff --git a/src/ipa-bcfier-ui/src/app/components/issue-filters/issue-filters.component.ts b/src/ipa-bcfier-ui/src/app/components/issue-filters/issue-filters.component.ts new file mode 100644 index 00000000..452acfb8 --- /dev/null +++ b/src/ipa-bcfier-ui/src/app/components/issue-filters/issue-filters.component.ts @@ -0,0 +1,104 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, + inject, +} from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; + +import { AsyncPipe } from '@angular/common'; + +import { Observable } from 'rxjs'; +import { + FormControl, + FormBuilder, + FormGroup, + ReactiveFormsModule, +} from '@angular/forms'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { provideNativeDateAdapter } from '@angular/material/core'; + +export interface IFilters { + status: FormControl; + type: FormControl; + users: FormControl; + issueRange: FormGroup<{ + start: FormControl; + end: FormControl; + }>; +} + +@Component({ + selector: 'bcfier-issue-filters', + standalone: true, + imports: [ + MatFormFieldModule, + MatSelectModule, + MatButtonModule, + AsyncPipe, + ReactiveFormsModule, + MatDatepickerModule, + ], + providers: [ + provideNativeDateAdapter({ + parse: { dateInput: { month: 'short', year: 'numeric', day: 'numeric' } }, + display: { + dateInput: 'input', + monthYearLabel: { year: 'numeric', month: 'short' }, + dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' }, + monthYearA11yLabel: { year: 'numeric', month: 'long' }, + }, + }), + ], + styleUrl: './issue-filters.component.scss', + templateUrl: './issue-filters.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class IssueFiltersComponent { + @Input({ + required: true, + }) + issueStatuses$!: Observable>; + + @Input({ + required: true, + }) + issueTypes$!: Observable>; + + //TODO replace type any + @Input({ + required: true, + }) + users$!: Observable; + + @Output() + acceptedFilters = new EventEmitter>(); + + fb = inject(FormBuilder); + + filtersForm: FormGroup; + constructor() { + this.filtersForm = this.fb.group({ + status: new FormControl('', { nonNullable: true }), + type: new FormControl('', { nonNullable: true }), + users: new FormControl('', { nonNullable: true }), + issueRange: new FormGroup({ + start: new FormControl(null), + end: new FormControl(null), + }), + }); + } + + acceptFilters(): void { + this.acceptedFilters.emit(this.filtersForm); + } + + clearFilters(): void { + this.filtersForm.reset() + this.acceptedFilters.emit(this.filtersForm); + } +} diff --git a/src/ipa-bcfier-ui/src/app/components/topic-detail/topic-detail.component.html b/src/ipa-bcfier-ui/src/app/components/topic-detail/topic-detail.component.html index 6b066d5b..4ca5047b 100644 --- a/src/ipa-bcfier-ui/src/app/components/topic-detail/topic-detail.component.html +++ b/src/ipa-bcfier-ui/src/app/components/topic-detail/topic-detail.component.html @@ -12,31 +12,41 @@ Status - @for (status of extensions.topicStatuses; track status) { + @for (status of (issueStatuses$ | async)?.values(); track status) { {{ status }} } - Type - @for (type of extensions.topicTypes; track type) { + @for (type of (issueTypes$ | async)?.values(); track type) { {{ type }} } - + + + + Responsible + + @for (user of (users$ | async)?.values(); track user) { + {{ user }} + } + + + + + Due Date + + MM/DD/YYYY + + +
diff --git a/src/ipa-bcfier-ui/src/app/components/topic-detail/topic-detail.component.ts b/src/ipa-bcfier-ui/src/app/components/topic-detail/topic-detail.component.ts index a5dd92aa..c7a8158a 100644 --- a/src/ipa-bcfier-ui/src/app/components/topic-detail/topic-detail.component.ts +++ b/src/ipa-bcfier-ui/src/app/components/topic-detail/topic-detail.component.ts @@ -3,7 +3,7 @@ import { BcfProjectExtensions, BcfTopic, } from '../../../generated/models'; -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, inject } from '@angular/core'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { AddStringValueComponent } from '../add-string-value/add-string-value.component'; @@ -17,6 +17,11 @@ import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; +import { IssueStatusesService } from '../../services/issue-statuses.service'; +import { IssueTypesService } from '../../services/issue-types.service'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { provideNativeDateAdapter } from '@angular/material/core'; +import { UsersService } from '../../services/users.service'; @Component({ selector: 'bcfier-topic-detail', @@ -33,15 +38,21 @@ import { MatSelectModule } from '@angular/material/select'; AddStringValueComponent, CommentsViewpointFilterPipe, CommentsDetailComponent, + MatDatepickerModule, ], + providers: [provideNativeDateAdapter()], templateUrl: './topic-detail.component.html', styleUrl: './topic-detail.component.scss', }) export class TopicDetailComponent implements OnInit { @Input() topic!: BcfTopic; @Input() bcfFile!: BcfFile; - + issueStatusesService = inject(IssueStatusesService); + issueTypesService = inject(IssueTypesService); + users$ = inject(UsersService).users; extensions!: BcfProjectExtensions; + issueStatuses$ = this.issueStatusesService.issueStatuses; + issueTypes$ = this.issueTypesService.issueTypes; constructor( private matDialog: MatDialog, @@ -49,6 +60,17 @@ export class TopicDetailComponent implements OnInit { ) {} ngOnInit(): void { + if (this.bcfFile?.projectExtensions?.topicStatuses) { + this.issueStatusesService.setIssueStatuses( + this.bcfFile?.projectExtensions?.topicStatuses + ); + } + + if (this.bcfFile?.projectExtensions?.topicTypes) { + this.issueTypesService.setIssueTypes( + this.bcfFile?.projectExtensions?.topicTypes + ); + } this.extensions = this.bcfFile?.projectExtensions || { priorities: [], snippetTypes: [], diff --git a/src/ipa-bcfier-ui/src/app/pipes/safe-url.pipe.ts b/src/ipa-bcfier-ui/src/app/pipes/safe-url.pipe.ts new file mode 100644 index 00000000..fab417fd --- /dev/null +++ b/src/ipa-bcfier-ui/src/app/pipes/safe-url.pipe.ts @@ -0,0 +1,36 @@ +import { Pipe, type PipeTransform } from '@angular/core'; +import { + DomSanitizer, + SafeHtml, + SafeResourceUrl, + SafeScript, + SafeStyle, + SafeUrl, +} from '@angular/platform-browser'; + +@Pipe({ + name: 'bcfierSafeUrl', + standalone: true, +}) +export class SafeUrlPipe implements PipeTransform { + constructor(protected _sanitizer: DomSanitizer) {} + transform( + value: string, + type: string + ): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl { + switch (type) { + case 'html': + return this._sanitizer.bypassSecurityTrustHtml(value); + case 'style': + return this._sanitizer.bypassSecurityTrustStyle(value); + case 'script': + return this._sanitizer.bypassSecurityTrustScript(value); + case 'url': + return this._sanitizer.bypassSecurityTrustUrl(value); + case 'resourceUrl': + return this._sanitizer.bypassSecurityTrustResourceUrl(value); + default: + return this._sanitizer.bypassSecurityTrustHtml(value); + } + } +} diff --git a/src/ipa-bcfier-ui/src/app/services/issue-filter.service.ts b/src/ipa-bcfier-ui/src/app/services/issue-filter.service.ts new file mode 100644 index 00000000..83363919 --- /dev/null +++ b/src/ipa-bcfier-ui/src/app/services/issue-filter.service.ts @@ -0,0 +1,61 @@ +import { Injectable, inject } from '@angular/core'; +import { BcfTopic } from '../../generated/models'; + +@Injectable({ + providedIn: 'root', +}) +export class IssueFilterService { + filterIssue( + issues: BcfTopic[], + status: string, + type: string, + users: any, + dateStart: Date | null, + dateEnd: Date | null + ): BcfTopic[] { + if (!issues || !issues.length) { + return []; + } + return issues.filter((issue) => { + let passesStatus = true; + let passesType = true; + let passesUsers = true; + let passesDate = true; + + if (status && issue.topicStatus !== status) { + passesStatus = false; + } + + if (type && issue.topicType !== type) { + passesType = false; + } + + //TODO add filter by user + // if (users && users.length > 0 && !users.includes(issue.user)) { + // passesUsers = false; + // } + + if ( + !!issue.dueDate && + dateStart && + new Date(issue.dueDate).getTime() < new Date(dateStart).getTime() + ) { + passesDate = false; + } + + if ( + dateEnd && + !!issue.dueDate && + new Date(issue.dueDate).getTime() > new Date(dateEnd).getTime() + ) { + passesDate = false; + } + + if ((dateStart || dateEnd) && !issue.dueDate) { + passesDate = false; + } + + return passesStatus && passesType && passesDate && passesUsers; + }); + } +} diff --git a/src/ipa-bcfier-ui/src/app/services/issue-statuses.service.ts b/src/ipa-bcfier-ui/src/app/services/issue-statuses.service.ts new file mode 100644 index 00000000..bb8ec02c --- /dev/null +++ b/src/ipa-bcfier-ui/src/app/services/issue-statuses.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +export const IssueStatuses = new Set([ + 'New', + 'Active', + 'Resolved', + 'Reviewed', + 'Approved', +]); +@Injectable({ + providedIn: 'root', +}) +export class IssueStatusesService { + private issueStatusesSource = new BehaviorSubject>(IssueStatuses); + issueStatuses = this.issueStatusesSource.asObservable(); + constructor() {} + + setIssueStatuses(statuses: string | string[]): void { + const statusArray = typeof statuses === 'string' ? [statuses] : statuses; + + const updatedStatuses = new Set([...IssueStatuses, ...statusArray]); + this.issueStatusesSource.next(updatedStatuses); + } +} diff --git a/src/ipa-bcfier-ui/src/app/services/issue-types.service.ts b/src/ipa-bcfier-ui/src/app/services/issue-types.service.ts new file mode 100644 index 00000000..d306bc4a --- /dev/null +++ b/src/ipa-bcfier-ui/src/app/services/issue-types.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +export const IssueTypes = new Set([ + 'Architecture', + 'Structural', + 'Electrical', + 'HVAC', + 'W&S', + 'Fire Extinguishing', + 'Gas', + 'Technology', +]); +@Injectable({ + providedIn: 'root', +}) +export class IssueTypesService { + private issueTypesSource = new BehaviorSubject>(IssueTypes); + issueTypes = this.issueTypesSource.asObservable(); + constructor() {} + + setIssueTypes(types: string | string[]): void { + const statusArray = typeof types === 'string' ? [types] : types; + + const updatedTypes = new Set([...IssueTypes, ...statusArray]); + this.issueTypesSource.next(updatedTypes); + } +} diff --git a/src/ipa-bcfier-ui/src/app/services/users.service.ts b/src/ipa-bcfier-ui/src/app/services/users.service.ts new file mode 100644 index 00000000..2e19a4b1 --- /dev/null +++ b/src/ipa-bcfier-ui/src/app/services/users.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { ReplaySubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class UsersService { + //TODO replace type any + private usersSource = new ReplaySubject(1); + users = this.usersSource.asObservable(); + constructor() { + this.getAllUsers(); + } + + //TODO replace type any + setUsers(users: any[]): void { + this.usersSource.next(users); + } + + getAllUsers(): void { + //TODO update, after add backend for getting users + this.setUsers(['Borys', 'Georg']); + } +}