Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Important notes (Dashboard Widget) #1297

Merged
merged 28 commits into from
Sep 20, 2022
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
db2402a
Implement important notes dashboard widget
Schottkyc137 May 28, 2022
e42c685
add types and default value
Schottkyc137 May 28, 2022
d7ac414
make the widget observable
Schottkyc137 May 28, 2022
aeef498
Add test
Schottkyc137 May 28, 2022
aea927f
cleaned up code
TheSlimvReal Jun 16, 2022
c7054cf
Merge remote-tracking branch 'origin/master' into important-notes
TheSlimvReal Jun 16, 2022
0fe98af
Merge branch 'master' into important-notes
sleidig Jul 5, 2022
a2593cd
Merge branch 'master' into important-notes
TheSlimvReal Jul 6, 2022
0ccee10
Merge remote-tracking branch 'origin/master' into important-notes
Schottkyc137 Aug 17, 2022
8dfa1f1
Merge Master; color background; sort
Schottkyc137 Aug 17, 2022
20fd73e
SonarCloud
Schottkyc137 Aug 17, 2022
ed8f943
Implement receiveUpdates for the mock entity mapper
Schottkyc137 Aug 18, 2022
19faf28
fix test
Schottkyc137 Aug 18, 2022
9859492
Merge branch 'master' into important-notes
TheSlimvReal Aug 23, 2022
17f153e
small review improvements
TheSlimvReal Aug 23, 2022
c6e450f
Removed console.log
TheSlimvReal Aug 23, 2022
a162916
Merge remote-tracking branch 'origin/important-notes' into important-…
TheSlimvReal Aug 23, 2022
15ca157
using expectObservable function
TheSlimvReal Aug 23, 2022
d3eaba4
Merge branch 'master' into important-notes
TheSlimvReal Aug 23, 2022
f795b0a
Merge remote-tracking branch 'origin/master' into important-notes
Schottkyc137 Aug 30, 2022
fe59e66
Testing notes highlighting style
Schottkyc137 Sep 1, 2022
69d4d50
Revert "Testing notes highlighting style"
Schottkyc137 Sep 8, 2022
606517d
add color left
Schottkyc137 Sep 8, 2022
a298e63
Add a color hint on the left side
Schottkyc137 Sep 8, 2022
db502a2
added left padding
TheSlimvReal Sep 20, 2022
2890399
Merge remote-tracking branch 'origin/master' into important-notes
TheSlimvReal Sep 20, 2022
364af16
fixed typo
TheSlimvReal Sep 20, 2022
4153632
removed unused import
TheSlimvReal Sep 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<app-dashboard-widget
icon="exclamation-triangle"
[title]="relevantNotes?.length"
subtitle="Notes needing follow-up"
i18n-subtitle="subtitle|dashboard showing notes that require action"
theme="note"
[loading]="loading"
>
<app-widget-content>
<div *ngIf="relevantNotes?.length > 0">
<table
mat-table
[dataSource]="notesDataSource"
aria-label="Notes needing follow-up"
>
<!-- Table header only for assistive technologies like screen readers -->
<tr hidden="true">
<th scope="col">Date</th>
<th scope="col">Title</th>
</tr>
<ng-container matColumnDef="date">
<td *matCellDef="let note">
{{ note.date | date }}
</td>
</ng-container>

<ng-container
matColumnDef="title"
>
<td *matCellDef="let note" class="subject-cell">
{{ note.subject }}
</td>
</ng-container>

<tr
mat-row
*matRowDef="let row; columns: ['date', 'title'];"
class="dashboard-table-row row-view"
(click)="openNote(row)"
Schottkyc137 marked this conversation as resolved.
Show resolved Hide resolved
[ngStyle]="{'--important-notes-bg-color': row.getColor?.()}"
></tr>
</table>
</div>
<div
*ngIf="relevantNotes?.length === 0"
class="headline"
>
<span i18n="Description when there are no notes that need a follow-up">
no notes that need immediate attention
</span>
</div>
<mat-paginator
#paginator
[style.display]="paginator.getNumberOfPages() === 0 ? 'none' : ''"
[pageSizeOptions]="[5]"
[hidePageSize]="true"
>
</mat-paginator>
</app-widget-content>
</app-dashboard-widget>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@use "../../../../core/dashboard/dashboard-widget-base";
@use "src/styles" as *;

.subject-cell {
text-align: right;
}

.row-view {
cursor: pointer;
position: relative;

&::before {
content: '';
display: block;
width: 8px;
height: 100%;
position: absolute;
background-color: var(--important-notes-bg-color);
}

& > td:first-child {
padding-left: $standard-margin-small + 4;
}

& > td:last-child {
padding-right: $standard-margin-small;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ComponentFixture, fakeAsync, TestBed } from "@angular/core/testing";

import { ImportantNotesComponent } from "./important-notes.component";
import { MockedTestingModule } from "../../../../utils/mocked-testing.module";
import { LoginState } from "../../../../core/session/session-states/login-state.enum";
import { Note } from "../../model/note";
import { FormDialogService } from "../../../../core/form-dialog/form-dialog.service";
import { MatPaginatorModule } from "@angular/material/paginator";
import { warningLevels } from "../../../warning-levels";

describe("ImportantNotesComponent", () => {
let component: ImportantNotesComponent;
let fixture: ComponentFixture<ImportantNotesComponent>;

const mockNotes = warningLevels.map((wLevel) => {
const note = Note.create(new Date());
note.warningLevel = wLevel;
return note;
});

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
MockedTestingModule.withState(LoginState.LOGGED_IN, mockNotes),
MatPaginatorModule,
],
declarations: [ImportantNotesComponent],
providers: [
{
provide: FormDialogService,
useValue: {
openDialog: () => {},
},
},
],
}).compileComponents();
});

beforeEach(async () => {
fixture = TestBed.createComponent(ImportantNotesComponent);
component = fixture.componentInstance;
component.onInitFromDynamicConfig({
warningLevels: ["WARNING", "URGENT"],
});
fixture.detectChanges();
await fixture.whenStable();
});

it("should create", () => {
expect(component).toBeTruthy();
});

it("shows notes that have a high warning level", fakeAsync(() => {
const expectedNotes = mockNotes
.filter((note) => ["WARNING", "URGENT"].includes(note.warningLevel.id))
.reverse();
expect(component.relevantNotes).toEqual(expectedNotes);
}));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { AfterViewInit, Component, OnInit, ViewChild } from "@angular/core";
import { Note } from "../../model/note";
import { EntityMapperService } from "../../../../core/entity/entity-mapper.service";
import { DynamicComponent } from "../../../../core/view/dynamic-components/dynamic-component.decorator";
import { OnInitDynamicComponent } from "../../../../core/view/dynamic-components/on-init-dynamic-component.interface";
import { MatTableDataSource } from "@angular/material/table";
import { MatPaginator } from "@angular/material/paginator";
import { FormDialogService } from "../../../../core/form-dialog/form-dialog.service";
import { NoteDetailsComponent } from "../../note-details/note-details.component";
import { applyUpdate } from "../../../../core/entity/model/entity-update";
import { concat, Observable } from "rxjs";
import { first, map } from "rxjs/operators";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";

@DynamicComponent("ImportantNotesComponent")
@UntilDestroy()
@Component({
selector: "app-important-notes",
templateUrl: "./important-notes.component.html",
styleUrls: ["./important-notes.component.scss"],
})
export class ImportantNotesComponent
implements OnInit, OnInitDynamicComponent, AfterViewInit
{
private relevantWarningLevels: string[] = [];
public relevantNotes: Note[];

private notes: Observable<Note[]>;
public loading: boolean = true;

public notesDataSource = new MatTableDataSource<Note>();

@ViewChild("paginator") private paginator: MatPaginator;

constructor(
private entityMapperService: EntityMapperService,
private formDialog: FormDialogService
) {}

ngOnInit(): void {
// This feed always contains the latest notes plus the initial notes
this.notes = concat(
this.entityMapperService.loadType(Note),
this.entityMapperService
.receiveUpdates(Note)
.pipe(map((next) => applyUpdate(this.relevantNotes, next)))
);
// set loading to `false` when the first chunk of notes (the initial notes) have arrived
this.notes.pipe(first()).subscribe(() => (this.loading = false));
this.notes.pipe(untilDestroyed(this)).subscribe((next) => {
this.relevantNotes = next.filter((note) => this.noteIsRelevant(note));
this.relevantNotes.sort(
(a, b) => b.warningLevel._ordinal - a.warningLevel._ordinal
);
this.notesDataSource.data = this.relevantNotes;
});
}

onInitFromDynamicConfig(config: any) {
this.relevantWarningLevels = config.warningLevels;
}

ngAfterViewInit() {
this.notesDataSource.paginator = this.paginator;
}

private noteIsRelevant(note: Note): boolean {
return this.relevantWarningLevels.includes(note.warningLevel.id);
}

openNote(note: Note) {
this.formDialog.openDialog(NoteDetailsComponent, note);
}
}
4 changes: 2 additions & 2 deletions src/app/child-dev-project/notes/model/note.ts
Original file line number Diff line number Diff line change
@@ -29,12 +29,12 @@ import {
} from "../../attendance/model/attendance-status";
import { User } from "../../../core/user/user";
import { Child } from "../../children/model/child";
import { ConfigurableEnumValue } from "../../../core/configurable-enum/configurable-enum.interface";
import {
getWarningLevelColor,
WarningLevel,
} from "../../../core/entity/model/warning-level";
import { School } from "../../schools/model/school";
import { Ordering } from "../../../core/configurable-enum/configurable-enum-ordering";

@DatabaseEntity("Note")
export class Note extends Entity {
@@ -113,7 +113,7 @@ export class Note extends Entity {
dataType: "configurable-enum",
innerDataType: "warning-levels",
})
warningLevel: ConfigurableEnumValue;
warningLevel: Ordering.EnumValue;

getWarningLevel(): WarningLevel {
if (this.warningLevel) {
3 changes: 3 additions & 0 deletions src/app/child-dev-project/notes/notes.module.ts
Original file line number Diff line number Diff line change
@@ -46,6 +46,7 @@ import { NotesDashboardComponent } from "./dashboard-widgets/notes-dashboard/not
import { NotesOfChildComponent } from "./notes-of-child/notes-of-child.component";
import { DashboardModule } from "../../core/dashboard/dashboard.module";
import { ExportModule } from "../../core/export/export.module";
import { ImportantNotesComponent } from "./dashboard-widgets/important-notes/important-notes.component";

@NgModule({
declarations: [
@@ -55,6 +56,7 @@ import { ExportModule } from "../../core/export/export.module";
NoteAttendanceCountBlockComponent,
NotesDashboardComponent,
NotesOfChildComponent,
ImportantNotesComponent,
],
imports: [
CommonModule,
@@ -112,5 +114,6 @@ export class NotesModule {
NoteAttendanceCountBlockComponent,
NotesDashboardComponent,
NotesOfChildComponent,
ImportantNotesComponent,
];
}
8 changes: 4 additions & 4 deletions src/app/child-dev-project/warning-levels.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ConfigurableEnumValue } from "../core/configurable-enum/configurable-enum.interface";
import { Ordering } from "../core/configurable-enum/configurable-enum-ordering";

export const warningLevels: ConfigurableEnumValue[] =
Ordering.imposeTotalOrdering([
export const warningLevels: Ordering.EnumValue[] = Ordering.imposeTotalOrdering(
[
{
id: "",
label: "",
@@ -19,4 +18,5 @@ export const warningLevels: ConfigurableEnumValue[] =
id: "URGENT",
label: $localize`:Label warning level:Urgent Follow-Up`,
},
]);
]
);
6 changes: 6 additions & 0 deletions src/app/core/config/config-fix.ts
Original file line number Diff line number Diff line change
@@ -170,6 +170,12 @@ export const defaultJsonConfig = {
{
"component": "ChildrenCountDashboard"
},
{
"component": "ImportantNotesComponent",
"config": {
"warningLevels": ["WARNING", "URGENT"],
}
},
{
"component": "NotesDashboard",
"config": {
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { DisplayConfigurableEnumComponent } from "./display-configurable-enum.component";
import { ConfigurableEnumValue } from "../configurable-enum.interface";
import { Note } from "../../../child-dev-project/notes/model/note";
import { Ordering } from "../configurable-enum-ordering";

describe("DisplayConfigurableEnumComponent", () => {
let component: DisplayConfigurableEnumComponent;
@@ -32,10 +32,11 @@ describe("DisplayConfigurableEnumComponent", () => {
const elem = fixture.debugElement.nativeElement;
expect(elem.style["background-color"]).toBe("");

const value: ConfigurableEnumValue = {
const value: Ordering.EnumValue = {
label: "withColor",
id: "WITH_COLOR",
color: "black",
_ordinal: 1,
};
const entity = new Note();
entity.warningLevel = value;
Original file line number Diff line number Diff line change
@@ -3,8 +3,8 @@
<div class="widget-header app-{{theme}}">
<div *ngIf="headline" class="widget-headline">{{headline}}</div>
<div *ngIf="!headline" class="widget-title">
<span *ngIf="!titleReady">{{_title}}</span>
<mat-spinner *ngIf="titleReady" diameter="40"></mat-spinner>
<span *ngIf="titleReady">{{_title}}</span>
<mat-spinner *ngIf="!titleReady" diameter="40"></mat-spinner>
</div>
<div *ngIf="!headline" class="widget-subheadline" [matTooltip]="explanation">{{subtitle}}</div>
<app-fa-dynamic-icon class="widget-icon" [icon]="icon"></app-fa-dynamic-icon>
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Component, Input } from "@angular/core";
import { IconName } from "@fortawesome/fontawesome-svg-core";
import { isPromise } from "../../../utils/utils";

export type DashboardTheme =
| "general"
@@ -15,23 +17,26 @@ export type DashboardTheme =
})
export class DashboardWidgetComponent {
@Input() subtitle: string;
@Input() icon: string;
@Input() icon: IconName;
@Input() theme: DashboardTheme;

_title: Promise<any>;
titleReady: boolean;
_title: string | number;
titleReady = true;

/** optional tooltip to explain detailed meaning of this widget / statistic */
@Input() explanation: string;
@Input() set title(title: PromiseLike<any> | any) {
if (title && typeof title["then"] === "function") {
this.titleReady = true;
@Input() set title(
title: PromiseLike<string | number> | string | number | undefined
) {
this.titleReady = false;
if (isPromise(title)) {
title.then((value) => {
this._title = value;
this.titleReady = false;
this.titleReady = true;
});
} else {
this._title = title;
this.titleReady = title !== undefined && title !== null;
}
}
@Input() headline: string;
Loading