mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Merge pull request #2910 from paperless-ngx/feature-improved-statistics-widget
Feature: Improved statistics widget
This commit is contained in:
@@ -1,6 +1,46 @@
|
||||
<app-widget-frame title="Statistics" [loading]="loading" i18n-title>
|
||||
<ng-container content>
|
||||
<p class="card-text" i18n *ngIf="statistics?.documents_inbox !== null">Documents in inbox: {{statistics?.documents_inbox}}</p>
|
||||
<p class="card-text" i18n>Total documents: {{statistics?.documents_total}}</p>
|
||||
<div class="list-group border-light">
|
||||
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to inbox" i18n-title href="javascript:void()" *ngIf="statistics?.documents_inbox !== null" (click)="goToInbox()">
|
||||
<ng-container i18n>Documents in inbox</ng-container>:
|
||||
<span class="badge rounded-pill" [class.bg-primary]="statistics?.documents_inbox > 0" [class.bg-muted]="statistics?.documents_inbox === 0">{{statistics?.documents_inbox}}</span>
|
||||
</a>
|
||||
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to documents" i18n-title routerLink="/documents/">
|
||||
<ng-container i18n>Total documents</ng-container>:
|
||||
<span class="badge bg-primary rounded-pill">{{statistics?.documents_total}}</span>
|
||||
</a>
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documents/">
|
||||
<ng-container i18n>Total characters</ng-container>:
|
||||
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.character_count | number}}</span>
|
||||
</div>
|
||||
<div *ngIf="statistics?.document_file_type_counts?.length > 1" class="list-group-item filetypes">
|
||||
<div class="d-flex justify-content-between align-items-center my-2">
|
||||
<div class="progress flex-grow-1">
|
||||
<div *ngFor="let filetype of statistics?.document_file_type_counts; let i = index; let last = last"
|
||||
class="progress-bar bg-primary text-primary-contrast"
|
||||
role="progressbar"
|
||||
[ngbPopover]="getFileTypeName(filetype)"
|
||||
i18n-ngbPopover
|
||||
triggers="mouseenter:mouseleave"
|
||||
[attr.aria-label]="getFileTypeName(filetype)"
|
||||
[class.me-1px]="!last"
|
||||
[style.width]="getFileTypePercent(filetype) + '%'"
|
||||
[style.opacity]="getItemOpacity(i)"
|
||||
[attr.aria-valuenow]="getFileTypePercent(filetype)"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap align-items-start">
|
||||
<div class="d-flex" *ngFor="let filetype of statistics?.document_file_type_counts; let i = index">
|
||||
<div class="text-nowrap me-2">
|
||||
<span class="badge rounded-pill bg-primary d-inline-block p-0 me-1" [style.opacity]="getItemOpacity(i)"></span>
|
||||
<small class="text-nowrap"><span class="fw-bold">{{ getFileTypeExtension(filetype) }}</span> <span class="text-muted">({{getFileTypePercent(filetype) | number: '1.0-1'}}%)</span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</app-widget-frame>
|
||||
|
@@ -0,0 +1,10 @@
|
||||
.filetypes {
|
||||
.progress {
|
||||
height: 0.6rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
height: 0.6rem;
|
||||
width: 0.6rem;
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,23 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Observable, Subscription } from 'rxjs'
|
||||
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import * as mimeTypeNames from 'mime-names'
|
||||
|
||||
export interface Statistics {
|
||||
documents_total?: number
|
||||
documents_inbox?: number
|
||||
inbox_tag?: number
|
||||
document_file_type_counts?: DocumentFileType[]
|
||||
character_count?: number
|
||||
}
|
||||
|
||||
interface DocumentFileType {
|
||||
mime_type: string
|
||||
mime_type_count: number
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -19,7 +30,8 @@ export class StatisticsWidgetComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private consumerStatusService: ConsumerStatusService
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private documentListViewService: DocumentListViewService
|
||||
) {}
|
||||
|
||||
statistics: Statistics = {}
|
||||
@@ -34,10 +46,43 @@ export class StatisticsWidgetComponent implements OnInit, OnDestroy {
|
||||
this.loading = true
|
||||
this.getStatistics().subscribe((statistics) => {
|
||||
this.loading = false
|
||||
const fileTypeMax = 5
|
||||
if (statistics.document_file_type_counts?.length > fileTypeMax) {
|
||||
const others = statistics.document_file_type_counts.slice(fileTypeMax)
|
||||
statistics.document_file_type_counts =
|
||||
statistics.document_file_type_counts.slice(0, fileTypeMax)
|
||||
statistics.document_file_type_counts.push({
|
||||
mime_type: $localize`Other`,
|
||||
mime_type_count: others.reduce(
|
||||
(currentValue, documentFileType) =>
|
||||
documentFileType.mime_type_count + currentValue,
|
||||
0
|
||||
),
|
||||
})
|
||||
}
|
||||
this.statistics = statistics
|
||||
})
|
||||
}
|
||||
|
||||
getFileTypeExtension(filetype: DocumentFileType): string {
|
||||
return (
|
||||
mimeTypeNames[filetype.mime_type]?.extensions[0]?.toUpperCase() ??
|
||||
filetype.mime_type
|
||||
)
|
||||
}
|
||||
|
||||
getFileTypeName(filetype: DocumentFileType): string {
|
||||
return mimeTypeNames[filetype.mime_type]?.name ?? filetype.mime_type
|
||||
}
|
||||
|
||||
getFileTypePercent(filetype: DocumentFileType): number {
|
||||
return (filetype.mime_type_count / this.statistics?.documents_total) * 100
|
||||
}
|
||||
|
||||
getItemOpacity(i: number): number {
|
||||
return 1 - i / this.statistics?.document_file_type_counts.length
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reload()
|
||||
this.subscription = this.consumerStatusService
|
||||
@@ -50,4 +95,13 @@ export class StatisticsWidgetComponent implements OnInit, OnDestroy {
|
||||
ngOnDestroy(): void {
|
||||
this.subscription.unsubscribe()
|
||||
}
|
||||
|
||||
goToInbox() {
|
||||
this.documentListViewService.quickFilter([
|
||||
{
|
||||
rule_type: FILTER_HAS_TAGS_ALL,
|
||||
value: this.statistics.inbox_tag.toString(),
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
@@ -629,3 +629,7 @@ code {
|
||||
.accordion-button::after {
|
||||
filter: invert(0.5) saturate(0);
|
||||
}
|
||||
|
||||
.me-1px {
|
||||
margin-right: 1px !important;
|
||||
}
|
||||
|
@@ -231,6 +231,14 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
||||
.dropdown-menu {
|
||||
--bs-dropdown-color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.card .list-group-item {
|
||||
--bs-border-color: rgb(var(--bs-dark-rgb));
|
||||
|
||||
.bg-secondary {
|
||||
background-color: rgb(var(--bs-dark-rgb)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.color-scheme-dark {
|
||||
|
Reference in New Issue
Block a user