diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 45a54b4..30049d8 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -7,6 +7,7 @@ import { SettingsComponent } from './pages/settings/settings.component'; import { ImprintComponent } from './pages/imprint/imprint.component'; import { PrivacyComponent } from './pages/privacy/privacy.component'; import { ErrorComponent } from './pages/error/error.component'; +import { SummaryComponent } from './pages/summary/summary.component'; export const routes: Routes = [ { @@ -22,6 +23,11 @@ export const routes: Routes = [ page: (route: ActivatedRouteSnapshot) => route.params['page'] } }, + { + path: 'summary', + title: 'Auswertung | Why App', + component: SummaryComponent, + }, { path: 'blog', title: 'Blog | Why App', diff --git a/src/app/models/result.model.ts b/src/app/models/result.model.ts new file mode 100644 index 0000000..6e565a9 --- /dev/null +++ b/src/app/models/result.model.ts @@ -0,0 +1,15 @@ +import { InputValue } from "./content.model"; + +export type ResultValue = Result | Progress | Record | undefined; + +export interface Result { + [key: string]: ResultValue; + data?: Record; + progress: Progress; +} + +export interface Progress { + count: number; + total: number; + percent: number; +} \ No newline at end of file diff --git a/src/app/pages/page/page.component.ts b/src/app/pages/page/page.component.ts index 16a5eab..4b54ef9 100644 --- a/src/app/pages/page/page.component.ts +++ b/src/app/pages/page/page.component.ts @@ -109,7 +109,7 @@ export class PageComponent { completed: true, data: { ...data, - [pageReadTime]: elapsedTime + [pageReadTime]: elapsedTime / 1000 } }); } diff --git a/src/app/pages/start/start.component.html b/src/app/pages/start/start.component.html index fbd5015..ea42425 100644 --- a/src/app/pages/start/start.component.html +++ b/src/app/pages/start/start.component.html @@ -64,9 +64,9 @@

@if (pageProgressPercent($index, section.slug) > 99) { - Erledigt + Erledigt } @else { - Offen + Offen } diff --git a/src/app/pages/start/start.component.scss b/src/app/pages/start/start.component.scss index ca7d745..e869544 100644 --- a/src/app/pages/start/start.component.scss +++ b/src/app/pages/start/start.component.scss @@ -1,5 +1,5 @@ header { - margin-bottom: 4rem; + margin-bottom: 4rem; [mat-subheader] { color: var(--mat-card-subtitle-text-color);; @@ -76,7 +76,7 @@ small { .card { flex: 1 0 auto; - max-width: 400px; + max-width: 367px; .card-header { display: flex; @@ -95,9 +95,9 @@ small { .card-content { padding: 0; - img { + .thumbnail { width: 100%; - height: 200px; + max-height: 200px; object-fit: cover; } } diff --git a/src/app/pages/start/start.component.ts b/src/app/pages/start/start.component.ts index c1c2e92..08e001e 100644 --- a/src/app/pages/start/start.component.ts +++ b/src/app/pages/start/start.component.ts @@ -8,8 +8,9 @@ import { LoadingComponent } from '../../components/ui/loading/loading.component' import { ProgressSpinnerComponent } from '../../components/ui/progress-spinner/progress-spinner.component'; import { CommonService } from '../../services/common.service'; import { GuideService } from '../../services/guide.service'; -import { Progress, UserProgressService } from '../../services/user-progress.service'; +import { UserResultService } from '../../services/user-result.service'; import { Guide } from '../../models/guide.model'; +import { Result } from '../../models/result.model'; @Component({ selector: 'app-start', @@ -29,18 +30,18 @@ import { Guide } from '../../models/guide.model'; export class StartComponent { private readonly _commonService = inject(CommonService); private readonly _guideService = inject(GuideService); - private readonly _progressService = inject(UserProgressService); + private readonly _resultService = inject(UserResultService); private _resources: Record = {}; private _units!: Guide[]; - private _progress!: Progress[]; + private _results!: Result[]; private _greeting!: string; private _userName!: string; loading = true; async ngOnInit() { this._units = await this._guideService.dataPromise; - this._progress = await this._progressService.progressTree; + this._results = await this._resultService.resultTree; this._resources = await this._commonService.getResources('start'); this.setUserName(this._resources['user-names'] as string[]); @@ -87,10 +88,10 @@ export class StartComponent { } unitProgressPercent(unitIndex: number) { - return this._progress ? this._progress[unitIndex].percent : 0; + return this._results ? this._results[unitIndex].progress.percent : 0; } pageProgressPercent(unitIndex: number, pageId: string) { - return this._progress ? (this._progress[unitIndex][pageId]).percent : 0; + return this._results ? (this._results[unitIndex][pageId]).progress.percent : 0; } } diff --git a/src/app/pages/summary/summary.component.html b/src/app/pages/summary/summary.component.html new file mode 100644 index 0000000..6be0062 --- /dev/null +++ b/src/app/pages/summary/summary.component.html @@ -0,0 +1,47 @@ + + @for (unit of summary; track $index) { +
+

+ {{resource('unit')}} {{$index + 1}} +

+ {{title($index)}} + + {{unit.progress.percent}}% + +
+ + + @if (data(unit[page.slug])) { + + + + {{page.title}} + {{percent(unit[page.slug])}}% + + +
+ @for (item of data(unit[page.slug]) | keyvalue: doneAtLast; track $index) { + @if (item.key === doneKey) { +
+ + } @else { + +
+ {{format(item.value) || 'Keine Angabe'}} +
+ } + } +
+
+ } +
+
+ + } + + +
\ No newline at end of file diff --git a/src/app/pages/summary/summary.component.scss b/src/app/pages/summary/summary.component.scss new file mode 100644 index 0000000..2b19c98 --- /dev/null +++ b/src/app/pages/summary/summary.component.scss @@ -0,0 +1,42 @@ +.header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + + h2 { + font-size: large; + letter-spacing: .1rem; + } + + .sub { + font-size: large; + font-weight: normal; + } + + small { + font-size: 8pt; + } +} + +.content { + padding: 1rem; + line-height: 1.5; + + label { + display: flex; + align-items: center; + gap: .4rem; + color: var(--primary); + font-weight: 500; + } + + .value { + padding: 1rem; + font-weight: 100; + } +} + +.spacer { + margin-bottom: 4rem; +} \ No newline at end of file diff --git a/src/app/pages/summary/summary.component.spec.ts b/src/app/pages/summary/summary.component.spec.ts new file mode 100644 index 0000000..3fa64a2 --- /dev/null +++ b/src/app/pages/summary/summary.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SummaryComponent } from './summary.component'; + +describe('SummaryComponent', () => { + let component: SummaryComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SummaryComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SummaryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/summary/summary.component.ts b/src/app/pages/summary/summary.component.ts new file mode 100644 index 0000000..76fe108 --- /dev/null +++ b/src/app/pages/summary/summary.component.ts @@ -0,0 +1,100 @@ +import { Component, inject } from '@angular/core'; +import { CommonModule, KeyValue } from '@angular/common'; +import { MatAccordion, MatExpansionModule } from '@angular/material/expansion'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatIconModule } from '@angular/material/icon'; +import { SettingsComponent } from '../settings/settings.component'; +import { LoadingComponent } from '../../components/ui/loading/loading.component'; +import { ProgressSpinnerComponent } from '../../components/ui/progress-spinner/progress-spinner.component'; +import { GuideService } from '../../services/guide.service'; +import { Guide } from '../../models/guide.model'; +import { Result, ResultValue } from '../../models/result.model'; +import { InputDefinition, InputValue } from '../../models/content.model'; +import { Page } from '../../models/page.model'; +import { CommonService } from '../../services/common.service'; +import { UserResultService } from '../../services/user-result.service'; +import { pageReadTime } from '../../services/user-data.service'; + +@Component({ + selector: 'app-summary', + standalone: true, + imports: [ + CommonModule, + MatAccordion, + MatExpansionModule, + MatDividerModule, + MatIconModule, + LoadingComponent, + ProgressSpinnerComponent, + SettingsComponent + ], + templateUrl: './summary.component.html', + styleUrl: './summary.component.scss', +}) +export class SummaryComponent { + private readonly _commonService = inject(CommonService); + private readonly _guideService = inject(GuideService); + private readonly _resultService = inject(UserResultService); + + private _resources: Record = {}; + private _units!: Guide[]; + private _results!: Result[]; + readonly doneKey = pageReadTime; + loading = true; + + async ngOnInit() { + this._units = await this._guideService.dataPromise; + this._results = await this._resultService.resultTree; + this._resources = await this._commonService.getResources('start'); + + this.loading = false; + } + + get summary() { + return this._results; + } + + resource(key: string) { + return this._resources[key]; + } + + title(index: number) { + return this._units[index].title; + } + + pages(index: number) { + return this._units[index].pages; + } + + data(result: ResultValue) { + const data = (result).data; + if (data && data[this.doneKey]) { + return Object.keys(data).reduce((acc, key) => { + acc[key] = data[key]; + return acc; + }, {} as Record); + } + return null; + } + + percent(result: ResultValue) { + return (result).progress.percent; + } + + caption(page: Page, id: string) { + return page.content + .filter(item => item.type === 'stepper') + .flatMap(item => item.value as InputDefinition[]) + .find(item => item.value.id === id)?.value?.caption || id; + } + + format(value: InputValue) { + return Array.isArray(value) + ? value.join(', ') + : value; + } + + doneAtLast(a: KeyValue) { + return a.key.startsWith('__') ? 1 : -1; + } +} diff --git a/src/app/services/user-progress.service.spec.ts b/src/app/services/user-result.service.spec.ts similarity index 50% rename from src/app/services/user-progress.service.spec.ts rename to src/app/services/user-result.service.spec.ts index 2b016f4..26d7aa5 100644 --- a/src/app/services/user-progress.service.spec.ts +++ b/src/app/services/user-result.service.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { UserProgressService } from './user-progress.service'; +import { UserResultService } from './user-result.service'; -xdescribe('UserProgressService', () => { - let service: UserProgressService; +xdescribe('UserResultService', () => { + let service: UserResultService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(UserProgressService); + service = TestBed.inject(UserResultService); }); it('should be created', () => { diff --git a/src/app/services/user-progress.service.ts b/src/app/services/user-result.service.ts similarity index 61% rename from src/app/services/user-progress.service.ts rename to src/app/services/user-result.service.ts index 6b0a2ef..232fbf0 100644 --- a/src/app/services/user-progress.service.ts +++ b/src/app/services/user-result.service.ts @@ -1,28 +1,22 @@ import { Injectable, inject } from '@angular/core'; import { GuideService } from './guide.service'; import { FormContent } from '../models/content.model'; +import { Result } from '../models/result.model'; import { UserDataService } from './user-data.service'; -export interface Progress { - [key: string]: Progress | number; - count: number; - total: number; - percent: number; -} - @Injectable({ providedIn: 'root', }) -export class UserProgressService { +export class UserResultService { private readonly userDataService = inject(UserDataService); private readonly guideService = inject(GuideService); - get progressTree(): Promise { + get resultTree(): Promise { return this.guideService.dataPromise.then(data => { return Promise.all(data.map(async (guide, index) => { const userData = this.userDataService.getEntry(index); return guide.pages.then(pages => { - let unitData: Record = {}; + let unitData: Record = {}; pages.forEach(page => { const count = userData[page.slug] ? Object.keys(userData[page.slug]).length : 0; const total = page.content @@ -31,13 +25,27 @@ export class UserProgressService { const percent = Math.round(count / total * 100); unitData = { ...unitData, - [page.slug]: { count, total, percent } + [page.slug]: { + data: userData[page.slug], + progress: { + count, + total, + percent + } + } } }); - const count = Object.keys(unitData).reduce((acc, slug) => acc + unitData[slug].count, 0); - const total = Object.keys(unitData).reduce((acc, slug) => acc + unitData[slug].total, 0); + const count = Object.keys(unitData).reduce((acc, slug) => acc + unitData[slug].progress.count, 0); + const total = Object.keys(unitData).reduce((acc, slug) => acc + unitData[slug].progress.total, 0); const percent = Math.round(count / total * 100); - return { ...unitData, count, total, percent }; + return { + ...unitData, + progress: { + count, + total, + percent + } + }; }); })); });