mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-09 09:58:20 -05:00
modular dashboard
This commit is contained in:
parent
4b47f4929e
commit
fa5df5d28e
@ -41,6 +41,10 @@ import { TagsComponent } from './components/common/input/tags/tags.component';
|
|||||||
import { SortableDirective } from './directives/sortable.directive';
|
import { SortableDirective } from './directives/sortable.directive';
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
import { CookieService } from 'ngx-cookie-service';
|
||||||
import { CsrfInterceptor } from './interceptors/csrf.interceptor';
|
import { CsrfInterceptor } from './interceptors/csrf.interceptor';
|
||||||
|
import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component';
|
||||||
|
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component';
|
||||||
|
import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component';
|
||||||
|
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -74,7 +78,11 @@ import { CsrfInterceptor } from './interceptors/csrf.interceptor';
|
|||||||
SaveViewConfigDialogComponent,
|
SaveViewConfigDialogComponent,
|
||||||
DateTimeComponent,
|
DateTimeComponent,
|
||||||
TagsComponent,
|
TagsComponent,
|
||||||
SortableDirective
|
SortableDirective,
|
||||||
|
SavedViewWidgetComponent,
|
||||||
|
StatisticsWidgetComponent,
|
||||||
|
UploadFileWidgetComponent,
|
||||||
|
WidgetFrameComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@ -4,77 +4,22 @@
|
|||||||
|
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<ng-container *ngFor="let v of savedDashboardViews">
|
<app-widget-frame title="Saved views" *ngIf="savedViews.length == 0">
|
||||||
|
<p class="card-text">This space is reserved to display your saved views. Go to your documents and save a view
|
||||||
<div class="card mb-3 shadow">
|
to have it displayed
|
||||||
<div class="card-header">
|
here!</p>
|
||||||
<h5 class="card-title mb-0">{{v.viewConfig.title}}</h5>
|
</app-widget-frame>
|
||||||
</div>
|
|
||||||
<div class="card-body text-dark">
|
|
||||||
|
|
||||||
<table class="table table-sm table-hover table-borderless">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Created</th>
|
|
||||||
<th scope="col">Title</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let doc of v.documents" routerLink="/documents/{{doc.id}}">
|
|
||||||
<td>{{doc.created | date}}</td>
|
|
||||||
<td>{{doc.title}}<app-tag [tag]="t" *ngFor="let t of doc.tags" class="ml-1"></app-tag>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="savedDashboardViews.length == 0">
|
|
||||||
<div class="card mb-3 shadow">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title mb-0">Saved views</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body text-dark">
|
|
||||||
<p class="card-text">This space is reserved to display your saved views. Go to your documents and save a view
|
|
||||||
to have it displayed
|
|
||||||
here!</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<ng-container *ngFor="let v of savedViews">
|
||||||
|
<app-saved-view-widget [savedView]="v"></app-saved-view-widget>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
|
|
||||||
<div class="card mb-3 shadow">
|
<app-statistics-widget></app-statistics-widget>
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title mb-0">Statistics</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body text-dark">
|
|
||||||
<p class="card-text">Documents in inbox: {{statistics.documents_inbox}}</p>
|
|
||||||
<p class="card-text">Total documents: {{statistics.documents_total}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mb-3 shadow">
|
<app-upload-file-widget></app-upload-file-widget>
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title mb-0">Upload new documents</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body text-dark">
|
|
||||||
<form>
|
|
||||||
<ngx-file-drop
|
|
||||||
dropZoneLabel="Drop documents here or" (onFileDrop)="dropped($event)"
|
|
||||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)"
|
|
||||||
dropZoneClassName="bg-light card"
|
|
||||||
multiple="true"
|
|
||||||
contentClassName="justify-content-center d-flex align-items-center p-5"
|
|
||||||
[showBrowseBtn]=true
|
|
||||||
browseBtnClassName="btn btn-sm btn-outline-primary ml-2">
|
|
||||||
|
|
||||||
</ngx-file-drop>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -1,16 +1,6 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
|
||||||
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
|
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
|
||||||
import { Toast, ToastService } from 'src/app/services/toast.service';
|
|
||||||
import { environment } from 'src/environments/environment';
|
|
||||||
|
|
||||||
export interface Statistics {
|
|
||||||
documents_total?: number
|
|
||||||
documents_inbox?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
@ -19,53 +9,14 @@ export interface Statistics {
|
|||||||
})
|
})
|
||||||
export class DashboardComponent implements OnInit {
|
export class DashboardComponent implements OnInit {
|
||||||
|
|
||||||
constructor(private documentService: DocumentService, private toastService: ToastService,
|
constructor(
|
||||||
public savedViewConfigService: SavedViewConfigService, private http: HttpClient) { }
|
public savedViewConfigService: SavedViewConfigService) { }
|
||||||
|
|
||||||
|
|
||||||
savedDashboardViews = []
|
savedViews = []
|
||||||
statistics: Statistics = {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.savedViewConfigService.getDashboardConfigs().forEach(config => {
|
this.savedViews = this.savedViewConfigService.getDashboardConfigs()
|
||||||
this.documentService.list(1,10,config.sortField,config.sortDirection,config.filterRules).subscribe(result => {
|
|
||||||
this.savedDashboardViews.push({viewConfig: config, documents: result.results})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.getStatistics().subscribe(statistics => {
|
|
||||||
this.statistics = statistics
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatistics(): Observable<Statistics> {
|
|
||||||
return this.http.get(`${environment.apiBaseUrl}statistics/`)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public fileOver(event){
|
|
||||||
console.log(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
public fileLeave(event){
|
|
||||||
console.log(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
public dropped(files: NgxFileDropEntry[]) {
|
|
||||||
for (const droppedFile of files) {
|
|
||||||
if (droppedFile.fileEntry.isFile) {
|
|
||||||
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry;
|
|
||||||
console.log(fileEntry)
|
|
||||||
fileEntry.file((file: File) => {
|
|
||||||
console.log(file)
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('document', file, file.name)
|
|
||||||
this.documentService.uploadDocument(formData).subscribe(result => {
|
|
||||||
this.toastService.showToast(Toast.make("Information", "The document has been uploaded and will be processed by the consumer shortly."))
|
|
||||||
}, error => {
|
|
||||||
this.toastService.showToast(Toast.makeError("An error has occured while uploading the document. Sorry!"))
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
<app-widget-frame [title]="savedView.title">
|
||||||
|
|
||||||
|
<table class="table table-sm table-hover table-borderless">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Created</th>
|
||||||
|
<th scope="col">Title</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let doc of documents" routerLink="/documents/{{doc.id}}">
|
||||||
|
<td>{{doc.created | date}}</td>
|
||||||
|
<td>{{doc.title}}<app-tag [tag]="t" *ngFor="let t of doc.tags" class="ml-1"></app-tag>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</app-widget-frame>
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SavedViewWidgetComponent } from './saved-view-widget.component';
|
||||||
|
|
||||||
|
describe('SavedViewWidgetComponent', () => {
|
||||||
|
let component: SavedViewWidgetComponent;
|
||||||
|
let fixture: ComponentFixture<SavedViewWidgetComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ SavedViewWidgetComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SavedViewWidgetComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,26 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||||
|
import { SavedViewConfig } from 'src/app/data/saved-view-config';
|
||||||
|
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-saved-view-widget',
|
||||||
|
templateUrl: './saved-view-widget.component.html',
|
||||||
|
styleUrls: ['./saved-view-widget.component.scss']
|
||||||
|
})
|
||||||
|
export class SavedViewWidgetComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(private documentService: DocumentService) { }
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
savedView: SavedViewConfig
|
||||||
|
|
||||||
|
documents: PaperlessDocument[] = []
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.documentService.list(1,10,this.savedView.sortField,this.savedView.sortDirection,this.savedView.filterRules).subscribe(result => {
|
||||||
|
this.documents = result.results
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
<app-widget-frame title="Statistics">
|
||||||
|
<p class="card-text">Documents in inbox: {{statistics.documents_inbox}}</p>
|
||||||
|
<p class="card-text">Total documents: {{statistics.documents_total}}</p>
|
||||||
|
</app-widget-frame>
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { StatisticsWidgetComponent } from './statistics-widget.component';
|
||||||
|
|
||||||
|
describe('StatisticsWidgetComponent', () => {
|
||||||
|
let component: StatisticsWidgetComponent;
|
||||||
|
let fixture: ComponentFixture<StatisticsWidgetComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ StatisticsWidgetComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(StatisticsWidgetComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,33 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
||||||
|
export interface Statistics {
|
||||||
|
documents_total?: number
|
||||||
|
documents_inbox?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-statistics-widget',
|
||||||
|
templateUrl: './statistics-widget.component.html',
|
||||||
|
styleUrls: ['./statistics-widget.component.scss']
|
||||||
|
})
|
||||||
|
export class StatisticsWidgetComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
|
statistics: Statistics = {}
|
||||||
|
|
||||||
|
getStatistics(): Observable<Statistics> {
|
||||||
|
return this.http.get(`${environment.apiBaseUrl}statistics/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.getStatistics().subscribe(statistics => {
|
||||||
|
this.statistics = statistics
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
<app-widget-frame title="Upload new documents">
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<ngx-file-drop
|
||||||
|
dropZoneLabel="Drop documents here or" (onFileDrop)="dropped($event)"
|
||||||
|
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)"
|
||||||
|
dropZoneClassName="bg-light card"
|
||||||
|
multiple="true"
|
||||||
|
contentClassName="justify-content-center d-flex align-items-center p-5"
|
||||||
|
[showBrowseBtn]=true
|
||||||
|
browseBtnClassName="btn btn-sm btn-outline-primary ml-2">
|
||||||
|
|
||||||
|
</ngx-file-drop>
|
||||||
|
</form>
|
||||||
|
</app-widget-frame>
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { UploadFileWidgetComponent } from './upload-file-widget.component';
|
||||||
|
|
||||||
|
describe('UploadFileWidgetComponent', () => {
|
||||||
|
let component: UploadFileWidgetComponent;
|
||||||
|
let fixture: ComponentFixture<UploadFileWidgetComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ UploadFileWidgetComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(UploadFileWidgetComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,44 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop';
|
||||||
|
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||||
|
import { Toast, ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-upload-file-widget',
|
||||||
|
templateUrl: './upload-file-widget.component.html',
|
||||||
|
styleUrls: ['./upload-file-widget.component.scss']
|
||||||
|
})
|
||||||
|
export class UploadFileWidgetComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(private documentService: DocumentService, private toastService: ToastService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
public fileOver(event){
|
||||||
|
console.log(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public fileLeave(event){
|
||||||
|
console.log(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public dropped(files: NgxFileDropEntry[]) {
|
||||||
|
for (const droppedFile of files) {
|
||||||
|
if (droppedFile.fileEntry.isFile) {
|
||||||
|
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry;
|
||||||
|
console.log(fileEntry)
|
||||||
|
fileEntry.file((file: File) => {
|
||||||
|
console.log(file)
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('document', file, file.name)
|
||||||
|
this.documentService.uploadDocument(formData).subscribe(result => {
|
||||||
|
this.toastService.showToast(Toast.make("Information", "The document has been uploaded and will be processed by the consumer shortly."))
|
||||||
|
}, error => {
|
||||||
|
this.toastService.showToast(Toast.makeError("An error has occured while uploading the document. Sorry!"))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
<div class="card mb-3 shadow">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">{{title}}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-dark">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { WidgetFrameComponent } from './widget-frame.component';
|
||||||
|
|
||||||
|
describe('WidgetFrameComponent', () => {
|
||||||
|
let component: WidgetFrameComponent;
|
||||||
|
let fixture: ComponentFixture<WidgetFrameComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ WidgetFrameComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(WidgetFrameComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-widget-frame',
|
||||||
|
templateUrl: './widget-frame.component.html',
|
||||||
|
styleUrls: ['./widget-frame.component.scss']
|
||||||
|
})
|
||||||
|
export class WidgetFrameComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
title: string
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user