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 @@
+
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']);
+ }
+}