Add the collection dashboard
One GET /api/stats call, aggregated in a single pass over the library.
Completion funnel, breakdowns by system, genre, decade, condition and
rating, a backlog that links into the filtered library, and a value card.
Form was picked before colour, and most of the page is not a chart: single
numbers are stat tiles, the breakdowns are bar lists with the value printed
per row, which is also the table view.
The colour work, in order:
* one hue for the breakdown bars — identity is on the axis labels, so
colour has nothing to encode, and a darker-where-bigger ramp would just
double-encode bar length
* an ordinal ramp for the funnel, since owned/played/finished are ordered
stages rather than peers
* validated with the dataviz validator against this app's real card
surfaces rather than a reference one, which caught that the documented
ordinal light-end measures 1.91:1 here and fails the 2:1 floor; the ramp
starts a step darker
* status colour used once, on the stale-valuation notice, with an icon and
text so it never carries meaning alone
Two bugs found by rendering it and looking, which the validator cannot see:
* the ratings card was showing condition data under a ratings heading — a
chart whose title did not describe its contents. Fixed by adding a real
rating distribution rather than relabelling the card.
* dark mode rendered light cards on a dark page. Copying the reference
pattern's `color-scheme` onto the container overrode how every
descendant resolved light-dark(), and the `:root`-prefixed media
override never matched at all, because Angular's emulated encapsulation
scopes selectors in component styles. Both replaced by light-dark()
values that inherit the app's own scheme.
The value card reports coverage, age and source beside the total, and flags
valuations older than 90 days, because a bare figure mixes fresh with stale
and silently omits everything unpriced.
148 backend tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export interface CountByLabel {
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
/** Each stage is a subset of the one before, so these read in order. */
|
||||
export interface CompletionFunnel {
|
||||
owned: number;
|
||||
played: number;
|
||||
finished: number;
|
||||
}
|
||||
|
||||
export interface ValueSummary {
|
||||
total: number;
|
||||
pricedCount: number;
|
||||
unpricedCount: number;
|
||||
totalPaid: number;
|
||||
paidCount: number;
|
||||
oldestValuedAt: string | null;
|
||||
newestValuedAt: string | null;
|
||||
sources: string[];
|
||||
totalIfCib: number | null;
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
totalGames: number;
|
||||
funnel: CompletionFunnel;
|
||||
backlog: number;
|
||||
inProgress: number;
|
||||
dumped: number;
|
||||
ratedCount: number;
|
||||
averageRating: number | null;
|
||||
bySystem: CountByLabel[];
|
||||
byGenre: CountByLabel[];
|
||||
byDecade: CountByLabel[];
|
||||
byCondition: CountByLabel[];
|
||||
byRating: CountByLabel[];
|
||||
value: ValueSummary;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class StatsService {
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
get(): Observable<Stats> {
|
||||
return this.http.get<Stats>('/api/stats');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user