Refactor dossier listing & overview

This commit is contained in:
Adina Țeudan 2021-09-30 02:00:47 +03:00
parent efa23a6e67
commit ef5e5ded18
60 changed files with 1423 additions and 1247 deletions

View File

@ -4,7 +4,7 @@ import { AppStateService } from '@state/app-state.service';
import { UserService } from '@services/user.service';
import { Toaster } from '@iqser/common-ui';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Dossier } from '../../../../state/model/dossier';
import { Dossier } from '@state/model/dossier';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
@ -119,6 +119,13 @@ export class TeamMembersManagerComponent implements OnInit {
this._loadData();
}
setMembersSelectOptions(): void {
this.membersSelectOptions = this.userService.eligibleUsers
.filter(user => this.userService.getNameForId(user.id).toLowerCase().includes(this.searchQuery.toLowerCase()))
.filter(user => this.selectedOwnerId !== user.id)
.map(user => user.id);
}
private _updateChanged() {
if (this.dossier.ownerId !== this.selectedOwnerId) {
this.changed = true;
@ -138,13 +145,6 @@ export class TeamMembersManagerComponent implements OnInit {
this.selectedReviewersList = this.selectedMembersList.filter(m => this.selectedApproversList.indexOf(m) === -1);
}
setMembersSelectOptions(): void {
this.membersSelectOptions = this.userService.eligibleUsers
.filter(user => this.userService.getNameForId(user.id).toLowerCase().includes(this.searchQuery.toLowerCase()))
.filter(user => this.selectedOwnerId !== user.id)
.map(user => user.id);
}
private _loadData() {
this.teamForm = this._formBuilder.group({
owner: [this.dossier?.ownerId, Validators.required],

View File

@ -69,7 +69,6 @@ export class AssignReviewerApproverDialogComponent {
const selectedUser = this.selectedSingleUser;
if (this.data.mode === 'reviewer') {
console.log('assign reviewer');
await this._filesService
.setReviewerFor(
this.data.files.map(f => f.fileId),

View File

@ -1,13 +1,11 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DossierListingScreenComponent } from './screens/dossier-listing-screen/dossier-listing-screen.component';
import { CompositeRouteGuard } from '@guards/composite-route.guard';
import { AuthGuard } from '../auth/auth.guard';
import { RedRoleGuard } from '../auth/red-role.guard';
import { AppStateGuard } from '@state/app-state.guard';
import { SearchScreenComponent } from './screens/search-screen/search-screen.component';
import { FilePreviewScreenComponent } from './screens/file-preview-screen/file-preview-screen.component';
import { DossierOverviewScreenComponent } from './screens/dossier-overview-screen/dossier-overview-screen.component';
const routes: Routes = [
{
@ -20,12 +18,12 @@ const routes: Routes = [
},
{
path: ':dossierId',
component: DossierOverviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard],
reuse: true
}
},
loadChildren: () => import('./screens/dossier-overview/dossier-overview.module').then(m => m.DossierOverviewModule)
},
{
path: ':dossierId/file/:fileId',
@ -39,12 +37,12 @@ const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: DossierListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard],
reuse: true
}
},
loadChildren: () => import('./screens/dossiers-listing/dossiers-listing.module').then(m => m.DossiersListingModule)
}
];

View File

@ -1,7 +1,5 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DossierListingScreenComponent } from './screens/dossier-listing-screen/dossier-listing-screen.component';
import { DossierOverviewScreenComponent } from './screens/dossier-overview-screen/dossier-overview-screen.component';
import { FilePreviewScreenComponent } from './screens/file-preview-screen/file-preview-screen.component';
import { AddDossierDialogComponent } from './dialogs/add-dossier-dialog/add-dossier-dialog.component';
import { AssignReviewerApproverDialogComponent } from './dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component';
@ -11,17 +9,9 @@ import { RemoveAnnotationsDialogComponent } from './dialogs/remove-annotations-d
import { DocumentInfoDialogComponent } from './dialogs/document-info-dialog/document-info-dialog.component';
import { PdfViewerComponent } from './components/pdf-viewer/pdf-viewer.component';
import { CommentsComponent } from './components/comments/comments.component';
import { DossierDetailsComponent } from './components/dossier-details/dossier-details.component';
import { PageIndicatorComponent } from './components/page-indicator/page-indicator.component';
import { NeedsWorkBadgeComponent } from './components/needs-work-badge/needs-work-badge.component';
import { AnnotationActionsComponent } from './components/annotation-actions/annotation-actions.component';
import { DossierListingDetailsComponent } from './components/dossier-listing-details/dossier-listing-details.component';
import { FileActionsComponent } from './components/file-actions/file-actions.component';
import { TypeAnnotationIconComponent } from './components/type-annotation-icon/type-annotation-icon.component';
import { TypeFilterComponent } from './components/type-filter/type-filter.component';
import { DossierOverviewBulkActionsComponent } from './components/bulk-actions/dossier-overview-bulk-actions.component';
import { TeamMembersComponent } from './components/team-members/team-members.component';
import { DossierListingActionsComponent } from './components/dossier-listing-actions/dossier-listing-actions.component';
import { DocumentInfoComponent } from './components/document-info/document-info.component';
import { FileWorkloadComponent } from './components/file-workload/file-workload.component';
import { SharedModule } from '@shared/shared.module';
@ -29,7 +19,6 @@ import { DossiersRoutingModule } from './dossiers-routing.module';
import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module';
import { DossiersDialogService } from './services/dossiers-dialog.service';
import { AnnotationActionsService } from './services/annotation-actions.service';
import { FileActionService } from './services/file-action.service';
import { PdfViewerDataService } from './services/pdf-viewer-data.service';
import { ManualAnnotationService } from './services/manual-annotation.service';
import { AnnotationDrawService } from './services/annotation-draw.service';
@ -46,14 +35,14 @@ import { PageExclusionComponent } from './components/page-exclusion/page-exclusi
import { RecategorizeImageDialogComponent } from './dialogs/recategorize-image-dialog/recategorize-image-dialog.component';
import { EditDossierAttributesComponent } from './dialogs/edit-dossier-dialog/attributes/edit-dossier-attributes.component';
import { DossiersService } from './services/dossiers.service';
import { DossierDetailsStatsComponent } from './components/dossier-details-stats/dossier-details-stats.component';
import { SearchScreenComponent } from './screens/search-screen/search-screen.component';
import { EditDossierDeletedDocumentsComponent } from './dialogs/edit-dossier-dialog/deleted-documents/edit-dossier-deleted-documents.component';
import { AnnotationsListComponent } from './components/file-workload/components/annotations-list/annotations-list.component';
import { AnnotationSourceComponent } from './components/file-workload/components/annotation-source/annotation-source.component';
import { OverlayModule } from '@angular/cdk/overlay';
import { SharedDossiersModule } from './shared/shared-dossiers.module';
const screens = [DossierListingScreenComponent, DossierOverviewScreenComponent, FilePreviewScreenComponent, SearchScreenComponent];
const screens = [FilePreviewScreenComponent, SearchScreenComponent];
const dialogs = [
AddDossierDialogComponent,
@ -70,17 +59,9 @@ const dialogs = [
const components = [
PdfViewerComponent,
CommentsComponent,
DossierDetailsComponent,
PageIndicatorComponent,
NeedsWorkBadgeComponent,
AnnotationActionsComponent,
DossierListingDetailsComponent,
TypeAnnotationIconComponent,
TypeFilterComponent,
DossierOverviewBulkActionsComponent,
FileActionsComponent,
TeamMembersComponent,
DossierListingActionsComponent,
DocumentInfoComponent,
FileWorkloadComponent,
EditDossierGeneralInfoComponent,
@ -90,7 +71,6 @@ const components = [
EditDossierAttributesComponent,
TeamMembersManagerComponent,
PageExclusionComponent,
DossierDetailsStatsComponent,
EditDossierDeletedDocumentsComponent,
AnnotationsListComponent,
AnnotationSourceComponent,
@ -102,7 +82,6 @@ const components = [
const services = [
DossiersService,
DossiersDialogService,
FileActionService,
AnnotationActionsService,
ManualAnnotationService,
PdfViewerDataService,
@ -114,6 +93,6 @@ const services = [
@NgModule({
declarations: [...components],
providers: [...services],
imports: [CommonModule, SharedModule, FileUploadDownloadModule, DossiersRoutingModule, OverlayModule]
imports: [CommonModule, SharedModule, SharedDossiersModule, FileUploadDownloadModule, DossiersRoutingModule, OverlayModule]
})
export class DossiersModule {}

View File

@ -1,86 +0,0 @@
<section>
<redaction-page-header [buttonConfigs]="buttonConfigs"></redaction-page-header>
<div class="overlay-shadow"></div>
<div class="red-content-inner">
<div class="content-container">
<iqser-table
(noDataAction)="openAddDossierDialog()"
[hasScrollButton]="true"
[itemSize]="85"
[noDataButtonLabel]="'dossier-listing.no-data.action' | translate"
[noDataText]="'dossier-listing.no-data.title' | translate"
[noMatchText]="'dossier-listing.no-match.title' | translate"
[showNoDataButton]="currentUser.isManager"
noDataIcon="red:folder"
></iqser-table>
</div>
<div class="right-container" iqserHasScrollbar>
<redaction-dossier-listing-details
*ngIf="(entitiesService.noData$ | async) === false"
[documentsChartData]="documentsChartData"
[dossiersChartData]="dossiersChartData"
></redaction-dossier-listing-details>
</div>
</div>
</section>
<ng-template #needsWorkFilterTemplate let-filter="filter">
<redaction-type-filter [filter]="filter"></redaction-type-filter>
</ng-template>
<ng-template #nameTemplate let-dossier="entity">
<div class="cell">
<div [matTooltip]="dossier.dossierName" class="table-item-title heading mb-6" matTooltipPosition="above">
{{ dossier.dossierName }}
</div>
<div class="small-label stats-subtitle mb-6">
<div>
<mat-icon svgIcon="red:template"></mat-icon>
{{ getDossierTemplateNameFor(dossier.dossierTemplateId) }}
</div>
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:document"></mat-icon>
{{ dossier.filesLength }}
</div>
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ dossier.totalNumberOfPages }}
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ dossier.memberIds.length }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ dossier.date | date: 'mediumDate' }}
</div>
<div *ngIf="dossier.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ dossier.dueDate | date: 'mediumDate' }}
</div>
</div>
</div>
</ng-template>
<ng-template #needsWorkTemplate let-dossier="entity">
<div class="cell">
<redaction-needs-work-badge [needsWorkInput]="dossier"></redaction-needs-work-badge>
</div>
</ng-template>
<ng-template #ownerTemplate let-dossier="entity">
<div class="cell user-column">
<redaction-initials-avatar [userId]="dossier.ownerId" [withName]="true"></redaction-initials-avatar>
</div>
</ng-template>
<ng-template #statusTemplate let-dossier="entity">
<div class="cell status-container">
<redaction-dossier-listing-actions (actionPerformed)="calculateData()" [dossier]="dossier"></redaction-dossier-listing-actions>
</div>
</ng-template>

View File

@ -1,22 +0,0 @@
:host {
::ng-deep iqser-table cdk-virtual-scroll-viewport .cdk-virtual-scroll-content-wrapper .table-item > div.cell {
&.status-container {
width: 160px;
}
}
.right-container {
display: flex;
width: 466px;
min-width: 466px;
padding-right: 11px;
&.has-scrollbar:hover {
padding-right: 0;
}
redaction-dossier-listing-details {
min-width: 466px;
}
}
}

View File

@ -1,336 +0,0 @@
import { AfterViewInit, Component, forwardRef, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { DossierStatuses } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { UserService } from '@services/user.service';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { groupBy } from '@utils/functions';
import { TranslateService } from '@ngx-translate/core';
import { Dossier } from '@state/model/dossier';
import { timer } from 'rxjs';
import { tap } from 'rxjs/operators';
import { TranslateChartService } from '@services/translate-chart.service';
import { RedactionFilterSorter } from '@utils/sorters/redaction-filter-sorter';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { Router } from '@angular/router';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy';
import { UserPreferenceService } from '@services/user-preference.service';
import { ButtonConfig } from '@shared/components/page-header/models/button-config.model';
import { DefaultListingServices, keyChecker, ListingComponent, NestedFilter, TableColumnConfig, TableComponent } from '@iqser/common-ui';
import { workloadTranslations } from '../../translations/workload-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { fileStatusTranslations } from '../../translations/file-status-translations';
import { annotationFilterChecker, dossierMemberChecker, dossierStatusChecker, dossierTemplateChecker } from '@utils/filter-utils';
import { PermissionsService } from '@services/permissions.service';
@Component({
templateUrl: './dossier-listing-screen.component.html',
styleUrls: ['./dossier-listing-screen.component.scss'],
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => DossierListingScreenComponent) }]
})
export class DossierListingScreenComponent
extends ListingComponent<Dossier>
implements OnInit, AfterViewInit, OnDestroy, OnAttach, OnDetach
{
readonly currentUser = this._userService.currentUser;
readonly tableHeaderLabel = _('dossier-listing.table-header.title');
readonly buttonConfigs: readonly ButtonConfig[] = [
{
label: _('dossier-listing.add-new'),
action: (): void => this.openAddDossierDialog(),
hide: !this.currentUser.isManager,
icon: 'red:plus',
type: 'primary'
}
];
tableColumnConfigs: TableColumnConfig<Dossier>[];
dossiersChartData: DoughnutChartConfig[] = [];
documentsChartData: DoughnutChartConfig[] = [];
@ViewChild('nameTemplate', { static: true }) nameTemplate: TemplateRef<unknown>;
@ViewChild('needsWorkTemplate', { static: true }) needsWorkTemplate: TemplateRef<unknown>;
@ViewChild('ownerTemplate', { static: true }) ownerTemplate: TemplateRef<unknown>;
@ViewChild('statusTemplate', { static: true }) statusTemplate: TemplateRef<unknown>;
private _lastScrolledIndex: number;
@ViewChild('needsWorkFilterTemplate', {
read: TemplateRef,
static: true
})
private readonly _needsWorkFilterTemplate: TemplateRef<unknown>;
@ViewChild(TableComponent) private readonly _tableComponent: TableComponent<Dossier>;
constructor(
private readonly _router: Router,
protected readonly _injector: Injector,
private readonly _userService: UserService,
readonly permissionsService: PermissionsService,
private readonly _appStateService: AppStateService,
private readonly _translateService: TranslateService,
private readonly _dialogService: DossiersDialogService,
private readonly _translateChartService: TranslateChartService,
private readonly _userPreferenceService: UserPreferenceService
) {
super(_injector);
this._appStateService.reset();
this._loadEntitiesFromState();
}
private get _activeDossiersCount(): number {
return this.entitiesService.all.filter(p => p.status === DossierStatuses.ACTIVE).length;
}
private get _inactiveDossiersCount(): number {
return this.entitiesService.all.length - this._activeDossiersCount;
}
getDossierTemplateNameFor(dossierTemplateId: string): string {
return this._appStateService.getDossierTemplateById(dossierTemplateId).name;
}
ngOnInit(): void {
this._configureTableColumns();
this.calculateData();
this.addSubscription = timer(0, 10000).subscribe(async () => {
await this._appStateService.loadAllDossiers();
this._loadEntitiesFromState();
this.calculateData();
});
this.addSubscription = this._appStateService.fileChanged$.subscribe(() => {
this.calculateData();
});
}
ngAfterViewInit(): void {
this.addSubscription = this._tableComponent.scrollViewport.scrolledIndexChange
.pipe(tap(index => (this._lastScrolledIndex = index)))
.subscribe();
}
ngOnAttach(): void {
this._appStateService.reset();
this._loadEntitiesFromState();
this.ngOnInit();
this.ngAfterViewInit();
this._tableComponent.scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
}
ngOnDetach(): void {
this.ngOnDestroy();
}
openAddDossierDialog(): void {
this._dialogService.openDialog('addDossier', null, null, async addResponse => {
await this._router.navigate([`/main/dossiers/${addResponse.dossier.id}`]);
if (addResponse.addMembers) {
this._dialogService.openDialog('editDossier', null, {
dossier: addResponse.dossier,
section: 'members'
});
}
});
}
calculateData(): void {
this._computeAllFilters();
this.dossiersChartData = [
{ value: this._activeDossiersCount, color: 'ACTIVE', label: _('active') },
{ value: this._inactiveDossiersCount, color: 'DELETED', label: _('archived') }
];
const groups = groupBy(this._appStateService.aggregatedFiles, 'status');
this.documentsChartData = [];
for (const status of Object.keys(groups)) {
this.documentsChartData.push({
value: groups[status].length,
color: status,
label: fileStatusTranslations[status],
key: status
});
}
this.documentsChartData.sort(StatusSorter.byStatus);
this.documentsChartData = this._translateChartService.translateStatus(this.documentsChartData);
}
private _configureTableColumns() {
this.tableColumnConfigs = [
{
label: _('dossier-listing.table-col-names.name'),
sortByKey: 'searchKey',
template: this.nameTemplate,
width: '2fr'
},
{
label: _('dossier-listing.table-col-names.needs-work'),
template: this.needsWorkTemplate
},
{
label: _('dossier-listing.table-col-names.owner'),
class: 'user-column',
template: this.ownerTemplate
},
{
label: _('dossier-listing.table-col-names.status'),
class: 'flex-end',
template: this.statusTemplate,
width: 'auto'
}
];
}
private _loadEntitiesFromState() {
this.entitiesService.setEntities(this._appStateService.allDossiers);
}
private _computeAllFilters() {
const allDistinctFileStatus = new Set<string>();
const allDistinctPeople = new Set<string>();
const allDistinctNeedsWork = new Set<string>();
const allDistinctDossierTemplates = new Set<string>();
this.entitiesService.all?.forEach(entry => {
// all people
entry.memberIds.forEach(f => allDistinctPeople.add(f));
// Needs work
entry.files.forEach(file => {
allDistinctFileStatus.add(file.status);
if (file.analysisRequired) {
allDistinctNeedsWork.add('analysis');
}
if (entry.hintsOnly) {
allDistinctNeedsWork.add('hint');
}
if (entry.hasRedactions) {
allDistinctNeedsWork.add('redaction');
}
if (entry.hasSuggestions) {
allDistinctNeedsWork.add('suggestion');
}
if (entry.hasNone) {
allDistinctNeedsWork.add('none');
}
});
allDistinctDossierTemplates.add(entry.dossierTemplateId);
});
const statusFilters = [...allDistinctFileStatus].map(
status =>
new NestedFilter({
id: status,
label: this._translateService.instant(fileStatusTranslations[status])
})
);
this.filterService.addFilterGroup({
slug: 'statusFilters',
label: this._translateService.instant('filters.status'),
icon: 'red:status',
filters: statusFilters.sort((a, b) => StatusSorter[a.id] - StatusSorter[b.id]),
checker: dossierStatusChecker
});
const peopleFilters = [...allDistinctPeople].map(
userId =>
new NestedFilter({
id: userId,
label: this._userService.getNameForId(userId)
})
);
this.filterService.addFilterGroup({
slug: 'peopleFilters',
label: this._translateService.instant('filters.people'),
icon: 'red:user',
filters: peopleFilters,
checker: dossierMemberChecker
});
const needsWorkFilters = [...allDistinctNeedsWork].map(
type =>
new NestedFilter({
id: type,
label: workloadTranslations[type]
})
);
this.filterService.addFilterGroup({
slug: 'needsWorkFilters',
label: this._translateService.instant('filters.needs-work'),
icon: 'red:needs-work',
filterTemplate: this._needsWorkFilterTemplate,
filters: needsWorkFilters.sort((a, b) => RedactionFilterSorter[a.id] - RedactionFilterSorter[b.id]),
checker: annotationFilterChecker,
matchAll: true
});
const dossierTemplateFilters = [...allDistinctDossierTemplates].map(
id =>
new NestedFilter({
id: id,
label: this._appStateService.getDossierTemplateById(id).name
})
);
this.filterService.addFilterGroup({
slug: 'dossierTemplateFilters',
label: this._translateService.instant('filters.dossier-templates'),
icon: 'red:template',
hide: dossierTemplateFilters.length <= 1,
filters: dossierTemplateFilters,
checker: dossierTemplateChecker
});
const quickFilters = this._createQuickFilters();
this.filterService.addFilterGroup({
slug: 'quickFilters',
filters: quickFilters,
checker: (dw: Dossier) => quickFilters.reduce((acc, f) => acc || (f.checked && f.checker(dw)), false)
});
const dossierFilters = this.entitiesService.all.map(
dossier =>
new NestedFilter({
id: dossier.dossierName,
label: dossier.dossierName
})
);
this.filterService.addFilterGroup({
slug: 'dossierNameFilter',
label: this._translateService.instant('dossier-listing.filters.label'),
icon: 'red:folder',
filters: dossierFilters,
filterceptionPlaceholder: this._translateService.instant('dossier-listing.filters.search'),
checker: keyChecker('dossierName')
});
}
private _createQuickFilters(): NestedFilter[] {
const myDossiersLabel = this._translateService.instant('dossier-listing.quick-filters.my-dossiers');
const filters: NestedFilter[] = [
{
id: 'my-dossiers',
label: myDossiersLabel,
checker: (dw: Dossier) => dw.ownerId === this.currentUser.id
},
{
id: 'to-approve',
label: this._translateService.instant('dossier-listing.quick-filters.to-approve'),
checker: (dw: Dossier) => dw.approverIds.includes(this.currentUser.id)
},
{
id: 'to-review',
label: this._translateService.instant('dossier-listing.quick-filters.to-review'),
checker: (dw: Dossier) => dw.memberIds.includes(this.currentUser.id)
},
{
id: 'other',
label: this._translateService.instant('dossier-listing.quick-filters.other'),
checker: (dw: Dossier) => !dw.memberIds.includes(this.currentUser.id)
}
].map(filter => new NestedFilter(filter));
return filters.filter(f => f.label === myDossiersLabel || this._userPreferenceService.areDevFeaturesEnabled);
}
}

View File

@ -1,601 +0,0 @@
import {
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
HostListener,
Injector,
OnDestroy,
OnInit,
TemplateRef,
ViewChild
} from '@angular/core';
import { FileStatus, FileStatuses, IFileAttributeConfig } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { FileDropOverlayService } from '@upload-download/services/file-drop-overlay.service';
import { FileUploadModel } from '@upload-download/model/file-upload.model';
import { FileUploadService } from '@upload-download/services/file-upload.service';
import { StatusOverlayService } from '@upload-download/services/status-overlay.service';
import { TranslateService } from '@ngx-translate/core';
import * as moment from 'moment';
import { DossierDetailsComponent } from '../../components/dossier-details/dossier-details.component';
import { File } from '@models/file/file';
import { UserService } from '@services/user.service';
import { timer } from 'rxjs';
import { tap } from 'rxjs/operators';
import { RedactionFilterSorter } from '@utils/sorters/redaction-filter-sorter';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { convertFiles, Files, handleFileDrop } from '@utils/file-drop-utils';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy';
import { ConfigService } from '@services/config.service';
import { ActionConfig } from '@shared/components/page-header/models/action-config.model';
import {
CircleButtonTypes,
DefaultListingServices,
INestedFilter,
keyChecker,
ListingComponent,
ListingModes,
LoadingService,
NestedFilter,
TableColumnConfig,
TableComponent,
Toaster,
WorkflowConfig
} from '@iqser/common-ui';
import { DossierAttributesService } from '@shared/services/controller-wrappers/dossier-attributes.service';
import { DossierAttributeWithValue } from '@models/dossier-attributes.model';
import { workloadTranslations } from '../../translations/workload-translations';
import { fileStatusTranslations } from '../../translations/file-status-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { annotationFilterChecker } from '@utils/filter-utils';
import { PermissionsService } from '@services/permissions.service';
import { RouterHistoryService } from '@services/router-history.service';
import { Dossier } from '@state/model/dossier';
import { Router } from '@angular/router';
import { FileActionService } from '../../services/file-action.service';
import { FileAttributesService } from '../../services/file-attributes.service';
@Component({
templateUrl: './dossier-overview-screen.component.html',
styleUrls: ['./dossier-overview-screen.component.scss'],
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => DossierOverviewScreenComponent) }]
})
export class DossierOverviewScreenComponent extends ListingComponent<File> implements OnInit, OnDestroy, OnDetach, OnAttach {
readonly listingModes = ListingModes;
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
currentDossier = this._appStateService.activeDossier;
readonly tableHeaderLabel = _('dossier-overview.table-header.title');
readonly actionConfigs: readonly ActionConfig[] = [
{
label: this._translateService.instant('dossier-overview.header-actions.edit'),
action: ($event): void => this.openEditDossierDialog($event),
icon: 'iqser:edit',
hide: !this.currentUser.isManager
}
];
tableColumnConfigs: readonly TableColumnConfig<File>[] = [];
collapsedDetails = false;
dossierAttributes: DossierAttributeWithValue[] = [];
fileAttributeConfigs: IFileAttributeConfig[];
@ViewChild('filenameTemplate', { static: true }) filenameTemplate: TemplateRef<unknown>;
@ViewChild('addedOnTemplate', { static: true }) addedOnTemplate: TemplateRef<unknown>;
@ViewChild('attributeTemplate', { static: true }) attributeTemplate: TemplateRef<unknown>;
@ViewChild('needsWorkTemplate', { static: true }) needsWorkTemplate: TemplateRef<unknown>;
@ViewChild('reviewerTemplate', { static: true }) reviewerTemplate: TemplateRef<unknown>;
@ViewChild('pagesTemplate', { static: true }) pagesTemplate: TemplateRef<unknown>;
@ViewChild('statusTemplate', { static: true }) statusTemplate: TemplateRef<unknown>;
readonly workflowConfig: WorkflowConfig<File, FileStatus>;
@ViewChild(DossierDetailsComponent, { static: false })
private readonly _dossierDetailsComponent: DossierDetailsComponent;
private _lastScrolledIndex: number;
@ViewChild('needsWorkFilterTemplate', { read: TemplateRef, static: true })
private readonly _needsWorkFilterTemplate: TemplateRef<unknown>;
@ViewChild('fileInput') private readonly _fileInput: ElementRef;
@ViewChild(TableComponent) private readonly _tableComponent: TableComponent<Dossier>;
constructor(
private readonly _toaster: Toaster,
protected readonly _injector: Injector,
private readonly _router: Router,
private readonly _userService: UserService,
readonly permissionsService: PermissionsService,
private readonly _loadingService: LoadingService,
private readonly _appStateService: AppStateService,
readonly routerHistoryService: RouterHistoryService,
private readonly _configService: ConfigService,
private readonly _translateService: TranslateService,
private readonly _dialogService: DossiersDialogService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _fileUploadService: FileUploadService,
private readonly _statusOverlayService: StatusOverlayService,
private readonly _fileDropOverlayService: FileDropOverlayService,
private readonly _dossierAttributesService: DossierAttributesService,
private readonly _fileActionService: FileActionService,
private readonly _fileAttributesService: FileAttributesService
) {
super(_injector);
this._loadEntitiesFromState();
this.fileAttributeConfigs = this._fileAttributesService.getFileAttributeConfig(
this.currentDossier.dossierTemplateId
)?.fileAttributeConfigs;
this.workflowConfig = {
columnIdentifierFn: entity => entity.status,
itemVersionFn: (entity: File) => `${entity.lastUpdated}-${entity.numberOfAnalyses}`,
columns: [
{
label: fileStatusTranslations[FileStatuses.UNASSIGNED],
key: FileStatuses.UNASSIGNED,
enterFn: this.unassignFn,
enterPredicate: () => false,
color: '#D3D5DA'
},
{
label: fileStatusTranslations[FileStatuses.UNDER_REVIEW],
enterFn: this.underReviewFn,
enterPredicate: (file: File) =>
this.permissionsService.canSetUnderReview(file) ||
this.permissionsService.canAssignToSelf(file) ||
this.permissionsService.canAssignUser(file),
key: FileStatuses.UNDER_REVIEW,
color: '#FDBD00'
},
{
label: fileStatusTranslations[FileStatuses.UNDER_APPROVAL],
enterFn: this.underApprovalFn,
enterPredicate: (file: File) =>
this.permissionsService.canSetUnderApproval(file) || this.permissionsService.canUndoApproval(file),
key: FileStatuses.UNDER_APPROVAL,
color: '#374C81'
},
{
label: fileStatusTranslations[FileStatuses.APPROVED],
enterFn: this.approveFn,
enterPredicate: (file: File) => this.permissionsService.isReadyForApproval(file),
key: FileStatuses.APPROVED,
color: '#48C9F7'
}
]
};
}
get checkedRequiredFilters() {
return this.filterService.getGroup('quickFilters')?.filters.filter(f => f.required && f.checked);
}
get checkedNotRequiredFilters() {
return this.filterService.getGroup('quickFilters')?.filters.filter(f => !f.required && f.checked);
}
get displayedInFileListAttributes() {
return this.fileAttributeConfigs?.filter(config => config.displayedInFileList) || [];
}
unassignFn = async (file: File) => {
// TODO
console.log('unassign', file);
};
underReviewFn = (file: File) => {
this._fileActionService.assignFile('reviewer', null, file, () => this._loadingService.loadWhile(this.reloadDossiers()), true);
};
underApprovalFn = async (file: File) => {
if (this._appStateService.activeDossier.approverIds.length > 1) {
this._fileActionService.assignFile('approver', null, file, () => this._loadingService.loadWhile(this.reloadDossiers()), true);
} else {
this._loadingService.start();
await this._fileActionService.setFilesUnderApproval([file]).toPromise();
await this.reloadDossiers();
this._loadingService.stop();
}
};
approveFn = async (file: File) => {
this._loadingService.start();
await this._fileActionService.setFilesApproved([file]).toPromise();
await this.reloadDossiers();
this._loadingService.stop();
};
actionPerformed(action?: string, file?: File) {
this.calculateData();
if (action === 'navigate') {
this._router.navigate([file.routerLink]);
}
}
disabledFn = (fileStatus: File) => fileStatus.excluded;
lastOpenedFn = (fileStatus: File) => fileStatus.lastOpened;
async ngOnInit(): Promise<void> {
this._configureTableColumns();
this._loadingService.start();
try {
this._fileDropOverlayService.initFileDropHandling();
this.calculateData();
this.addSubscription = timer(0, 7500).subscribe(async () => {
await this._appStateService.reloadActiveDossierFilesIfNecessary();
this._loadEntitiesFromState();
});
this.addSubscription = this._appStateService.fileChanged$.subscribe(() => {
this.calculateData();
});
this.addSubscription = this._appStateService.dossierTemplateChanged$.subscribe(() => {
this.fileAttributeConfigs = this._fileAttributesService.getFileAttributeConfig(
this.currentDossier.dossierTemplateId
)?.fileAttributeConfigs;
});
this.addSubscription = this._tableComponent.scrollViewport.scrolledIndexChange
.pipe(tap(index => (this._lastScrolledIndex = index)))
.subscribe();
this.dossierAttributes = await this._dossierAttributesService.getValues(this.currentDossier);
} catch (e) {
console.log('Error from dossier overview screen: ', e);
} finally {
this._loadingService.stop();
}
}
ngOnDestroy(): void {
this._fileDropOverlayService.cleanupFileDropHandling();
super.ngOnDestroy();
}
async ngOnAttach() {
await this._appStateService.reloadActiveDossierFiles();
this._loadEntitiesFromState();
await this.ngOnInit();
this._tableComponent.scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
}
ngOnDetach() {
this.ngOnDestroy();
}
async reanalyseDossier() {
try {
await this._appStateService.reanalyzeDossier();
await this.reloadDossiers();
this._toaster.success(_('dossier-overview.reanalyse-dossier.success'));
} catch (e) {
this._toaster.error(_('dossier-overview.reanalyse-dossier.error'));
}
}
async reloadDossiers() {
await this._appStateService.getFiles(this.currentDossier, false);
this.calculateData();
}
calculateData(): void {
if (!this._appStateService.activeDossierId) {
return;
}
this._loadEntitiesFromState();
this._computeAllFilters();
this._dossierDetailsComponent?.calculateChartConfig();
this._changeDetectorRef.detectChanges();
}
@HostListener('drop', ['$event'])
onDrop(event: DragEvent): void {
handleFileDrop(event, this.currentDossier, this._uploadFiles.bind(this));
}
@HostListener('dragover', ['$event'])
onDragOver(event): void {
event.stopPropagation();
event.preventDefault();
}
async uploadFiles(files: Files): Promise<void> {
await this._uploadFiles(convertFiles(files, this.currentDossier));
this._fileInput.nativeElement.value = null;
}
async bulkActionPerformed(): Promise<void> {
this.entitiesService.setSelected([]);
await this.reloadDossiers();
}
openEditDossierDialog($event: MouseEvent) {
this._dialogService.openDialog('editDossier', $event, {
dossier: this.currentDossier
});
}
openAssignDossierMembersDialog(): void {
const data = { dossier: this.currentDossier, section: 'members' };
this._dialogService.openDialog('editDossier', null, data, async () => await this.reloadDossiers());
}
openDossierDictionaryDialog() {
const data = { dossier: this.currentDossier, section: 'dossierDictionary' };
this._dialogService.openDialog('editDossier', null, data, async () => {
await this.reloadDossiers();
});
}
toggleCollapsedDetails() {
this.collapsedDetails = !this.collapsedDetails;
}
recentlyModifiedChecker = (file: File) =>
moment(file.lastUpdated).add(this._configService.values.RECENT_PERIOD_IN_HOURS, 'hours').isAfter(moment());
private _configureTableColumns() {
const dynamicColumns: TableColumnConfig<File>[] = [];
for (const config of this.displayedInFileListAttributes) {
if (config.displayedInFileList) {
dynamicColumns.push({ label: config.label, notTranslatable: true, template: this.attributeTemplate, extra: config });
}
}
this.tableColumnConfigs = [
{
label: _('dossier-overview.table-col-names.name'),
sortByKey: 'searchKey',
template: this.filenameTemplate,
width: '3fr'
},
{
label: _('dossier-overview.table-col-names.added-on'),
sortByKey: 'added',
template: this.addedOnTemplate,
width: '2fr'
},
...dynamicColumns,
{
label: _('dossier-overview.table-col-names.needs-work'),
template: this.needsWorkTemplate
},
{
label: _('dossier-overview.table-col-names.assigned-to'),
class: 'user-column',
sortByKey: 'reviewerName',
template: this.reviewerTemplate,
width: '2fr'
},
{
label: _('dossier-overview.table-col-names.pages'),
sortByKey: 'numberOfPages',
template: this.pagesTemplate
},
{
label: _('dossier-overview.table-col-names.status'),
class: 'flex-end',
sortByKey: 'statusSort',
template: this.statusTemplate
}
];
}
private _loadEntitiesFromState() {
this.currentDossier = this._appStateService.activeDossier;
if (this.currentDossier) {
this.entitiesService.setEntities(this.currentDossier.files);
}
}
private async _uploadFiles(files: FileUploadModel[]) {
const fileCount = await this._fileUploadService.uploadFiles(files);
if (fileCount) {
this._statusOverlayService.openUploadStatusOverlay();
}
}
private _computeAllFilters() {
if (!this.currentDossier) {
return;
}
const allDistinctFileStatuses = new Set<string>();
const allDistinctPeople = new Set<string>();
const allDistinctAddedDates = new Set<string>();
const allDistinctNeedsWork = new Set<string>();
const dynamicFilters = new Map<string, Set<string>>();
this.entitiesService.all.forEach(file => {
allDistinctPeople.add(file.currentReviewer);
allDistinctFileStatuses.add(file.status);
allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY'));
if (file.analysisRequired) {
allDistinctNeedsWork.add('analysis');
}
if (file.hintsOnly) {
allDistinctNeedsWork.add('hint');
}
if (file.hasRedactions) {
allDistinctNeedsWork.add('redaction');
}
if (file.hasSuggestions) {
allDistinctNeedsWork.add('suggestion');
}
if (file.hasUpdates) {
allDistinctNeedsWork.add('updated');
}
if (file.hasImages) {
allDistinctNeedsWork.add('image');
}
if (file.hasNone) {
allDistinctNeedsWork.add('none');
}
if (file.hasAnnotationComments) {
allDistinctNeedsWork.add('comment');
}
// extract values for dynamic filters
this.fileAttributeConfigs.forEach(config => {
if (config.filterable) {
const filterKey = `${config.id}:${config.label}`;
let filters = dynamicFilters.get(filterKey);
if (!filters) {
dynamicFilters.set(filterKey, new Set<string>());
filters = dynamicFilters.get(filterKey);
}
let filterValue = file.fileAttributes?.attributeIdToValue[config.id];
if (!filterValue) {
filterValue = '-';
file.fileAttributes.attributeIdToValue[config.id] = '-';
}
filters.add(filterValue);
}
});
});
const statusFilters = [...allDistinctFileStatuses].map(
status =>
new NestedFilter({
id: status,
label: this._translateService.instant(fileStatusTranslations[status])
})
);
this.filterService.addFilterGroup({
slug: 'statusFilters',
label: this._translateService.instant('filters.status'),
icon: 'red:status',
filters: statusFilters.sort((a, b) => StatusSorter[a.id] - StatusSorter[b.id]),
checker: keyChecker('status')
});
const peopleFilters: NestedFilter[] = [];
if (allDistinctPeople.has(undefined) || allDistinctPeople.has(null)) {
allDistinctPeople.delete(undefined);
allDistinctPeople.delete(null);
peopleFilters.push(
new NestedFilter({
id: null,
label: this._translateService.instant('initials-avatar.unassigned')
})
);
}
allDistinctPeople.forEach(userId => {
peopleFilters.push(
new NestedFilter({
id: userId,
label: this._userService.getNameForId(userId)
})
);
});
this.filterService.addFilterGroup({
slug: 'peopleFilters',
label: this._translateService.instant('filters.assigned-people'),
icon: 'red:user',
filters: peopleFilters,
checker: keyChecker('currentReviewer')
});
const needsWorkFilters = [...allDistinctNeedsWork].map(
item =>
new NestedFilter({
id: item,
label: workloadTranslations[item]
})
);
this.filterService.addFilterGroup({
slug: 'needsWorkFilters',
label: this._translateService.instant('filters.needs-work'),
icon: 'red:needs-work',
filterTemplate: this._needsWorkFilterTemplate,
filters: needsWorkFilters.sort(RedactionFilterSorter.byKey),
checker: annotationFilterChecker,
matchAll: true
});
dynamicFilters.forEach((filterValue: Set<string>, filterKey: string) => {
const id = filterKey.split(':')[0];
const key = filterKey.split(':')[1];
this.filterService.addFilterGroup({
slug: key,
label: key,
icon: 'red:template',
filters: [...filterValue].map(
(value: string) =>
new NestedFilter({
id: value,
label: value === '-' ? this._translateService.instant('filters.empty') : value
})
),
checker: (input: File, filter: INestedFilter) => filter.id === input.fileAttributes.attributeIdToValue[id]
});
});
this.filterService.addFilterGroup({
slug: 'quickFilters',
filters: this._createQuickFilters(),
checker: (file: File) =>
this.checkedRequiredFilters.reduce((acc, f) => acc && f.checker(file), true) &&
(this.checkedNotRequiredFilters.length === 0 ||
this.checkedNotRequiredFilters.reduce((acc, f) => acc || f.checker(file), false))
});
const filesNamesFilters = this.entitiesService.all.map(
file =>
new NestedFilter({
id: file.filename,
label: file.filename
})
);
this.filterService.addFilterGroup({
slug: 'filesNamesFilter',
label: this._translateService.instant('dossier-overview.filters.label'),
icon: 'red:document',
filters: filesNamesFilters,
checker: keyChecker('filename'),
filterceptionPlaceholder: this._translateService.instant('dossier-overview.filters.search')
});
}
private _createQuickFilters(): NestedFilter[] {
let quickFilters: INestedFilter[] = [];
if (this.entitiesService.all.filter(this.recentlyModifiedChecker).length > 0) {
const recentPeriod = this._configService.values.RECENT_PERIOD_IN_HOURS;
quickFilters = [
{
id: 'recent',
label: this._translateService.instant('dossier-overview.quick-filters.recent', {
hours: recentPeriod
}),
required: true,
checker: this.recentlyModifiedChecker
}
];
}
return [
...quickFilters,
{
id: 'assigned-to-me',
label: this._translateService.instant('dossier-overview.quick-filters.assigned-to-me'),
checker: (file: File) => file.currentReviewer === this.currentUser.id
},
{
id: 'unassigned',
label: this._translateService.instant('dossier-overview.quick-filters.unassigned'),
checker: (file: File) => !file.currentReviewer
},
{
id: 'assigned-to-others',
label: this._translateService.instant('dossier-overview.quick-filters.assigned-to-others'),
checker: (file: File) => !!file.currentReviewer && file.currentReviewer !== this.currentUser.id
}
].map(filter => new NestedFilter(filter));
}
}

View File

@ -1,13 +1,13 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { AppStateService } from '../../../../../../state/app-state.service';
import { FileManagementControllerService, ReanalysisControllerService } from '@redaction/red-ui-http';
import { PermissionsService } from '@services/permissions.service';
import { File } from '@models/file/file';
import { FileActionService } from '../../services/file-action.service';
import { PermissionsService } from '../../../../../../services/permissions.service';
import { File } from '../../../../../../models/file/file';
import { FileActionService } from '../../../../shared/services/file-action.service';
import { Observable } from 'rxjs';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
import { CircleButtonTypes, EntitiesService, LoadingService } from '@iqser/common-ui';
import { ConfirmationDialogInput } from '@shared/dialogs/confirmation-dialog/confirmation-dialog.component';
import { ConfirmationDialogInput } from '../../../../../shared/dialogs/confirmation-dialog/confirmation-dialog.component';
import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';

View File

@ -3,7 +3,7 @@ import { DossierAttributeWithValue } from '@models/dossier-attributes.model';
import { AppStateService } from '@state/app-state.service';
import { Dossier } from '@state/model/dossier';
import { IDossierTemplate } from '@redaction/red-ui-http';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
@Component({
selector: 'redaction-dossier-details-stats',

View File

@ -1,13 +1,12 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { groupBy } from '@utils/functions';
import { groupBy, StatusSorter } from '@utils/index';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { TranslateChartService } from '@services/translate-chart.service';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { UserService } from '@services/user.service';
import { FilterService, Toaster } from '@iqser/common-ui';
import { DossierAttributeWithValue } from '@models/dossier-attributes.model';
import { fileStatusTranslations } from '../../translations/file-status-translations';
import { fileStatusTranslations } from '../../../../translations/file-status-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { List } from '@redaction/red-ui-http';
import { User } from '@models/user';

View File

@ -0,0 +1,70 @@
<div class="cell">
<div>
<div [class.error]="file.isError" [matTooltip]="file.filename" class="table-item-title" matTooltipPosition="above">
{{ file.filename }}
</div>
</div>
<div *ngIf="file.primaryAttribute" class="small-label">
<div class="primary-attribute">
<span [matTooltip]="file.primaryAttribute" matTooltipPosition="above">
{{ file.primaryAttribute }}
</span>
</div>
</div>
<ng-container *ngTemplateOutlet="statsTemplate; context: { entity: file }"></ng-container>
</div>
<div class="cell">
<div [class.error]="file.isError" class="small-label">
{{ file.added | date: 'd MMM. yyyy, hh:mm a' }}
</div>
</div>
<div *ngFor="let config of displayedAttributes" class="cell">
{{ file.fileAttributes.attributeIdToValue[config.id] }}
</div>
<!-- always show A for error-->
<div *ngIf="file.isError" class="cell">
<redaction-annotation-icon color="#dd4d50" label="A" type="square"></redaction-annotation-icon>
</div>
<div *ngIf="!file.isError" class="cell">
<redaction-needs-work-badge [needsWorkInput]="file"></redaction-needs-work-badge>
</div>
<div *ngIf="!file.isError" class="user-column cell">
<redaction-initials-avatar [userId]="file.currentReviewer" [withName]="true"></redaction-initials-avatar>
</div>
<div *ngIf="!file.isError" class="cell">
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ file.numberOfPages }}
</div>
</div>
</div>
<div [class.extend-cols]="file.isError" class="status-container cell">
<div *ngIf="file.isError" class="small-label error" translate="dossier-overview.file-listing.file-entry.file-error"></div>
<div *ngIf="file.isPending" class="small-label" translate="dossier-overview.file-listing.file-entry.file-pending"></div>
<div *ngIf="file.isProcessing" class="small-label loading" translate="dossier-overview.file-listing.file-entry.file-processing"></div>
<iqser-status-bar
*ngIf="file.isWorkable"
[configs]="[
{
color: file.status,
length: 1
}
]"
></iqser-status-bar>
<redaction-file-actions
(actionPerformed)="calculateData.emit()"
*ngIf="!file.isProcessing"
[file]="file"
class="mr-4"
type="dossier-overview-list"
></redaction-file-actions>
</div>

View File

@ -0,0 +1,26 @@
@use 'common-mixins';
@use 'variables';
.cell {
.error {
color: variables.$primary;
}
.table-item-title {
max-width: 25vw;
}
.primary-attribute {
padding-top: 6px;
@include common-mixins.line-clamp(1);
}
&.extend-cols {
grid-column-end: span 3;
align-items: flex-end;
}
&.status-container {
align-items: flex-end;
}
}

View File

@ -0,0 +1,19 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, TemplateRef } from '@angular/core';
import { File } from '@models/file/file';
import { Required } from '@iqser/common-ui';
import { IFileAttributeConfig } from '@redaction/red-ui-http';
@Component({
selector: 'redaction-table-item',
templateUrl: './table-item.component.html',
styleUrls: ['./table-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TableItemComponent {
@Input() @Required() file!: File;
@Input() @Required() statsTemplate!: TemplateRef<unknown>;
@Input() @Required() displayedAttributes!: IFileAttributeConfig[];
@Output() readonly calculateData = new EventEmitter<void>();
constructor() {}
}

View File

@ -0,0 +1,364 @@
import { Injectable, TemplateRef } from '@angular/core';
import { IFilterGroup, INestedFilter, keyChecker, LoadingService, NestedFilter, TableColumnConfig, WorkflowConfig } from '@iqser/common-ui';
import { File } from '@models/file/file';
import { fileStatusTranslations } from '../../translations/file-status-translations';
import { FileStatus, FileStatuses, IFileAttributeConfig } from '@redaction/red-ui-http';
import { FileActionService } from '../../shared/services/file-action.service';
import { AppStateService } from '@state/app-state.service';
import { PermissionsService } from '@services/permissions.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core';
import { UserService } from '@services/user.service';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { annotationFilterChecker, RedactionFilterSorter, StatusSorter } from '@utils/index';
import { workloadTranslations } from '../../translations/workload-translations';
import * as moment from 'moment';
import { ConfigService as AppConfigService } from '@services/config.service';
@Injectable()
export class ConfigService {
constructor(
private readonly _fileActionService: FileActionService,
private readonly _loadingService: LoadingService,
private readonly _appStateService: AppStateService,
private readonly _permissionsService: PermissionsService,
private readonly _translateService: TranslateService,
private readonly _userService: UserService,
private readonly _dialogService: DossiersDialogService,
private readonly _appConfigService: AppConfigService
) {}
get actionConfig() {
return [
{
label: this._translateService.instant('dossier-overview.header-actions.edit'),
action: $event => this._openEditDossierDialog($event),
icon: 'iqser:edit',
hide: !this._userService.currentUser.isManager
}
];
}
tableConfig(displayedAttributes: IFileAttributeConfig[]): TableColumnConfig<File>[] {
const dynamicColumns: TableColumnConfig<File>[] = displayedAttributes.map(config => ({
label: config.label,
notTranslatable: true
}));
return [
{
label: _('dossier-overview.table-col-names.name'),
sortByKey: 'searchKey',
width: '3fr'
},
{
label: _('dossier-overview.table-col-names.added-on'),
sortByKey: 'added',
width: '2fr'
},
...dynamicColumns,
{
label: _('dossier-overview.table-col-names.needs-work')
},
{
label: _('dossier-overview.table-col-names.assigned-to'),
class: 'user-column',
sortByKey: 'reviewerName',
width: '2fr'
},
{
label: _('dossier-overview.table-col-names.pages'),
sortByKey: 'numberOfPages'
},
{
label: _('dossier-overview.table-col-names.status'),
class: 'flex-end',
sortByKey: 'statusSort'
}
];
}
workflowConfig(reloadDossiers: () => Promise<void>): WorkflowConfig<File, FileStatus> {
return {
columnIdentifierFn: entity => entity.status,
itemVersionFn: (entity: File) => `${entity.lastUpdated}-${entity.numberOfAnalyses}`,
columns: [
{
label: fileStatusTranslations[FileStatuses.UNASSIGNED],
key: FileStatuses.UNASSIGNED,
enterFn: this._unassignFn(reloadDossiers),
enterPredicate: () => false,
color: '#D3D5DA'
},
{
label: fileStatusTranslations[FileStatuses.UNDER_REVIEW],
enterFn: this._underReviewFn(reloadDossiers),
enterPredicate: (file: File) =>
this._permissionsService.canSetUnderReview(file) ||
this._permissionsService.canAssignToSelf(file) ||
this._permissionsService.canAssignUser(file),
key: FileStatuses.UNDER_REVIEW,
color: '#FDBD00'
},
{
label: fileStatusTranslations[FileStatuses.UNDER_APPROVAL],
enterFn: this._underApprovalFn(reloadDossiers),
enterPredicate: (file: File) =>
this._permissionsService.canSetUnderApproval(file) || this._permissionsService.canUndoApproval(file),
key: FileStatuses.UNDER_APPROVAL,
color: '#374C81'
},
{
label: fileStatusTranslations[FileStatuses.APPROVED],
enterFn: this._approveFn(reloadDossiers),
enterPredicate: (file: File) => this._permissionsService.isReadyForApproval(file),
key: FileStatuses.APPROVED,
color: '#48C9F7'
}
]
};
}
filterGroups(
entities: File[],
fileAttributeConfigs: IFileAttributeConfig[],
needsWorkFilterTemplate: TemplateRef<unknown>,
checkedRequiredFilters: () => NestedFilter[],
checkedNotRequiredFilters: () => NestedFilter[]
) {
const allDistinctFileStatuses = new Set<string>();
const allDistinctPeople = new Set<string>();
const allDistinctAddedDates = new Set<string>();
const allDistinctNeedsWork = new Set<string>();
const dynamicFilters = new Map<string, Set<string>>();
const filterGroups: IFilterGroup[] = [];
entities.forEach(file => {
allDistinctPeople.add(file.currentReviewer);
allDistinctFileStatuses.add(file.status);
allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY'));
if (file.analysisRequired) {
allDistinctNeedsWork.add('analysis');
}
if (file.hintsOnly) {
allDistinctNeedsWork.add('hint');
}
if (file.hasRedactions) {
allDistinctNeedsWork.add('redaction');
}
if (file.hasSuggestions) {
allDistinctNeedsWork.add('suggestion');
}
if (file.hasUpdates) {
allDistinctNeedsWork.add('updated');
}
if (file.hasImages) {
allDistinctNeedsWork.add('image');
}
if (file.hasNone) {
allDistinctNeedsWork.add('none');
}
if (file.hasAnnotationComments) {
allDistinctNeedsWork.add('comment');
}
// extract values for dynamic filters
fileAttributeConfigs.forEach(config => {
if (config.filterable) {
const filterKey = `${config.id}:${config.label}`;
let filters = dynamicFilters.get(filterKey);
if (!filters) {
dynamicFilters.set(filterKey, new Set<string>());
filters = dynamicFilters.get(filterKey);
}
let filterValue = file.fileAttributes?.attributeIdToValue[config.id];
if (!filterValue) {
filterValue = '-';
file.fileAttributes.attributeIdToValue[config.id] = '-';
}
filters.add(filterValue);
}
});
});
const statusFilters = [...allDistinctFileStatuses].map(
status =>
new NestedFilter({
id: status,
label: this._translateService.instant(fileStatusTranslations[status])
})
);
filterGroups.push({
slug: 'statusFilters',
label: this._translateService.instant('filters.status'),
icon: 'red:status',
filters: statusFilters.sort((a, b) => StatusSorter[a.id] - StatusSorter[b.id]),
checker: keyChecker('status')
});
const peopleFilters: NestedFilter[] = [];
if (allDistinctPeople.has(undefined) || allDistinctPeople.has(null)) {
allDistinctPeople.delete(undefined);
allDistinctPeople.delete(null);
peopleFilters.push(
new NestedFilter({
id: null,
label: this._translateService.instant('initials-avatar.unassigned')
})
);
}
allDistinctPeople.forEach(userId => {
peopleFilters.push(
new NestedFilter({
id: userId,
label: this._userService.getNameForId(userId)
})
);
});
filterGroups.push({
slug: 'peopleFilters',
label: this._translateService.instant('filters.assigned-people'),
icon: 'red:user',
filters: peopleFilters,
checker: keyChecker('currentReviewer')
});
const needsWorkFilters = [...allDistinctNeedsWork].map(
item =>
new NestedFilter({
id: item,
label: workloadTranslations[item]
})
);
filterGroups.push({
slug: 'needsWorkFilters',
label: this._translateService.instant('filters.needs-work'),
icon: 'red:needs-work',
filterTemplate: needsWorkFilterTemplate,
filters: needsWorkFilters.sort(RedactionFilterSorter.byKey),
checker: annotationFilterChecker,
matchAll: true
});
dynamicFilters.forEach((filterValue: Set<string>, filterKey: string) => {
const id = filterKey.split(':')[0];
const key = filterKey.split(':')[1];
filterGroups.push({
slug: key,
label: key,
icon: 'red:template',
filters: [...filterValue].map(
(value: string) =>
new NestedFilter({
id: value,
label: value === '-' ? this._translateService.instant('filters.empty') : value
})
),
checker: (input: File, filter: INestedFilter) => filter.id === input.fileAttributes.attributeIdToValue[id]
});
});
filterGroups.push({
slug: 'quickFilters',
filters: this._quickFilters(entities),
checker: (file: File) =>
checkedRequiredFilters().reduce((acc, f) => acc && f.checker(file), true) &&
(checkedNotRequiredFilters().length === 0 || checkedNotRequiredFilters().reduce((acc, f) => acc || f.checker(file), false))
});
const filesNamesFilters = entities.map(
file =>
new NestedFilter({
id: file.filename,
label: file.filename
})
);
filterGroups.push({
slug: 'filesNamesFilter',
label: this._translateService.instant('dossier-overview.filters.label'),
icon: 'red:document',
filters: filesNamesFilters,
checker: keyChecker('filename'),
filterceptionPlaceholder: this._translateService.instant('dossier-overview.filters.search')
});
return filterGroups;
}
_recentlyModifiedChecker = (file: File) =>
moment(file.lastUpdated).add(this._appConfigService.values.RECENT_PERIOD_IN_HOURS, 'hours').isAfter(moment());
private _quickFilters(entities: File[]): NestedFilter[] {
let quickFilters: INestedFilter[] = [];
if (entities.filter(this._recentlyModifiedChecker).length > 0) {
const recentPeriod = this._appConfigService.values.RECENT_PERIOD_IN_HOURS;
quickFilters = [
{
id: 'recent',
label: this._translateService.instant('dossier-overview.quick-filters.recent', {
hours: recentPeriod
}),
required: true,
checker: this._recentlyModifiedChecker
}
];
}
return [
...quickFilters,
{
id: 'assigned-to-me',
label: this._translateService.instant('dossier-overview.quick-filters.assigned-to-me'),
checker: (file: File) => file.currentReviewer === this._userService.currentUser.id
},
{
id: 'unassigned',
label: this._translateService.instant('dossier-overview.quick-filters.unassigned'),
checker: (file: File) => !file.currentReviewer
},
{
id: 'assigned-to-others',
label: this._translateService.instant('dossier-overview.quick-filters.assigned-to-others'),
checker: (file: File) => !!file.currentReviewer && file.currentReviewer !== this._userService.currentUser.id
}
].map(filter => new NestedFilter(filter));
}
private _openEditDossierDialog($event: MouseEvent) {
this._dialogService.openDialog('editDossier', $event, {
dossier: this._appStateService.activeDossier
});
}
private _unassignFn = (reloadDossiers: () => Promise<void>) => async (file: File) => {
// TODO
console.log('unassign', file);
};
private _underReviewFn = (reloadDossiers: () => Promise<void>) => (file: File) => {
this._fileActionService.assignFile('reviewer', null, file, () => this._loadingService.loadWhile(reloadDossiers()), true);
};
private _underApprovalFn = (reloadDossiers: () => Promise<void>) => async (file: File) => {
if (this._appStateService.activeDossier.approverIds.length > 1) {
this._fileActionService.assignFile('approver', null, file, () => this._loadingService.loadWhile(reloadDossiers()), true);
} else {
this._loadingService.start();
await this._fileActionService.setFilesUnderApproval([file]).toPromise();
await reloadDossiers();
this._loadingService.stop();
}
};
private _approveFn = (reloadDossiers: () => Promise<void>) => async (file: File) => {
this._loadingService.start();
await this._fileActionService.setFilesApproved([file]).toPromise();
await reloadDossiers();
this._loadingService.stop();
};
}

View File

@ -0,0 +1,34 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { SharedModule } from '@shared/shared.module';
import { IqserIconsModule } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { DossierOverviewScreenComponent } from './screen/dossier-overview-screen.component';
import { DossierOverviewBulkActionsComponent } from './components/bulk-actions/dossier-overview-bulk-actions.component';
import { DossierDetailsComponent } from './components/dossier-details/dossier-details.component';
import { DossierDetailsStatsComponent } from './components/dossier-details-stats/dossier-details-stats.component';
import { TableItemComponent } from './components/table-item/table-item.component';
import { ConfigService } from './config.service';
import { SharedDossiersModule } from '../../shared/shared-dossiers.module';
const routes = [
{
path: '',
component: DossierOverviewScreenComponent,
pathMatch: 'full'
}
];
@NgModule({
declarations: [
DossierOverviewScreenComponent,
DossierOverviewBulkActionsComponent,
DossierDetailsComponent,
DossierDetailsStatsComponent,
TableItemComponent
],
providers: [ConfigService],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, SharedDossiersModule, IqserIconsModule, TranslateModule]
})
export class DossierOverviewModule {}

View File

@ -91,95 +91,6 @@
<redaction-dossier-overview-bulk-actions (reload)="bulkActionPerformed()"></redaction-dossier-overview-bulk-actions>
</ng-template>
<ng-template #filenameTemplate let-file="entity">
<div class="cell">
<div>
<div [class.error]="file.isError" [matTooltip]="file.filename" class="table-item-title" matTooltipPosition="above">
{{ file.filename }}
</div>
</div>
<div *ngIf="file.primaryAttribute" class="small-label">
<div class="primary-attribute">
<span [matTooltip]="file.primaryAttribute" matTooltipPosition="above">
{{ file.primaryAttribute }}
</span>
</div>
</div>
<ng-container *ngTemplateOutlet="statsTemplate; context: { entity: file }"></ng-container>
</div>
</ng-template>
<ng-template #addedOnTemplate let-file="entity">
<div class="cell">
<div [class.error]="file.isError" class="small-label">
{{ file.added | date: 'd MMM. yyyy, hh:mm a' }}
</div>
</div>
</ng-template>
<ng-template #attributeTemplate let-config="extra" let-file="entity">
<div class="cell">
{{ file.fileAttributes.attributeIdToValue[config.id] }}
</div>
</ng-template>
<ng-template #needsWorkTemplate let-file="entity">
<!-- always show A for error-->
<div *ngIf="file.isError" class="cell">
<redaction-annotation-icon color="#dd4d50" label="A" type="square"></redaction-annotation-icon>
</div>
<div *ngIf="!file.isError" class="cell">
<redaction-needs-work-badge [needsWorkInput]="file"></redaction-needs-work-badge>
</div>
</ng-template>
<ng-template #reviewerTemplate let-file="entity">
<div *ngIf="!file.isError" class="user-column cell">
<redaction-initials-avatar [userId]="file.currentReviewer" [withName]="true"></redaction-initials-avatar>
</div>
</ng-template>
<ng-template #pagesTemplate let-file="entity">
<div *ngIf="!file.isError" class="cell">
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ file.numberOfPages }}
</div>
</div>
</div>
</ng-template>
<ng-template #statusTemplate let-file="entity">
<div [class.extend-cols]="file.isError" class="status-container cell">
<div *ngIf="file.isError" class="small-label error" translate="dossier-overview.file-listing.file-entry.file-error"></div>
<div *ngIf="file.isPending" class="small-label" translate="dossier-overview.file-listing.file-entry.file-pending"></div>
<div
*ngIf="file.isProcessing"
class="small-label loading"
translate="dossier-overview.file-listing.file-entry.file-processing"
></div>
<iqser-status-bar
*ngIf="file.isWorkable"
[configs]="[
{
color: file.status,
length: 1
}
]"
></iqser-status-bar>
<redaction-file-actions
(actionPerformed)="calculateData()"
*ngIf="!file.isProcessing"
[file]="file"
class="mr-4"
type="dossier-overview-list"
></redaction-file-actions>
</div>
</ng-template>
<ng-template #viewModeSelection>
<div class="view-mode-selection">
<div class="all-caps-label" translate="view-mode.view-as"></div>
@ -199,6 +110,15 @@
</div>
</ng-template>
<ng-template #tableItemTemplate let-file="entity">
<redaction-table-item
(calculateData)="calculateData()"
[displayedAttributes]="displayedAttributes"
[file]="file"
[statsTemplate]="statsTemplate"
></redaction-table-item>
</ng-template>
<ng-template #workflowItemTemplate let-file="entity">
<div class="workflow-item">
<div>

View File

@ -7,39 +7,17 @@
:host ::ng-deep iqser-table cdk-virtual-scroll-viewport .cdk-virtual-scroll-content-wrapper .table-item {
&.last-opened {
> .selection-column {
.selection-column {
padding-left: 6px !important;
border-left: 4px solid variables.$primary;
}
> div {
.selection-column,
.cell,
.scrollbar-placeholder {
animation: red-fading-background 3s 1;
}
}
> div.cell {
.error {
color: variables.$primary;
}
.table-item-title {
max-width: 25vw;
}
.primary-attribute {
padding-top: 6px;
@include common-mixins.line-clamp(1);
}
&.extend-cols {
grid-column-end: span 3;
align-items: flex-end;
}
&.status-container {
align-items: flex-end;
}
}
}
.right-container {

View File

@ -0,0 +1,280 @@
import {
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
HostListener,
Injector,
OnDestroy,
OnInit,
TemplateRef,
ViewChild
} from '@angular/core';
import { FileStatus, IFileAttributeConfig } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { FileDropOverlayService } from '@upload-download/services/file-drop-overlay.service';
import { FileUploadModel } from '@upload-download/model/file-upload.model';
import { FileUploadService } from '@upload-download/services/file-upload.service';
import { StatusOverlayService } from '@upload-download/services/status-overlay.service';
import { TranslateService } from '@ngx-translate/core';
import * as moment from 'moment';
import { DossierDetailsComponent } from '../components/dossier-details/dossier-details.component';
import { File } from '@models/file/file';
import { UserService } from '@services/user.service';
import { timer } from 'rxjs';
import { tap } from 'rxjs/operators';
import { convertFiles, Files, handleFileDrop, OnAttach, OnDetach } from '@utils/index';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { ActionConfig } from '@shared/components/page-header/models/action-config.model';
import {
CircleButtonTypes,
DefaultListingServices,
ListingComponent,
ListingModes,
LoadingService,
NestedFilter,
TableColumnConfig,
TableComponent,
Toaster,
WorkflowConfig
} from '@iqser/common-ui';
import { DossierAttributesService } from '@shared/services/controller-wrappers/dossier-attributes.service';
import { DossierAttributeWithValue } from '@models/dossier-attributes.model';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { PermissionsService } from '@services/permissions.service';
import { RouterHistoryService } from '@services/router-history.service';
import { Dossier } from '@state/model/dossier';
import { Router } from '@angular/router';
import { FileAttributesService } from '../../../services/file-attributes.service';
import { ConfigService as AppConfigService } from '@services/config.service';
import { ConfigService } from '../config.service';
@Component({
templateUrl: './dossier-overview-screen.component.html',
styleUrls: ['./dossier-overview-screen.component.scss'],
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => DossierOverviewScreenComponent) }]
})
export class DossierOverviewScreenComponent extends ListingComponent<File> implements OnInit, OnDestroy, OnDetach, OnAttach {
readonly listingModes = ListingModes;
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
currentDossier = this._appStateService.activeDossier;
readonly tableHeaderLabel = _('dossier-overview.table-header.title');
collapsedDetails = false;
dossierAttributes: DossierAttributeWithValue[] = [];
fileAttributeConfigs: IFileAttributeConfig[];
tableColumnConfigs: readonly TableColumnConfig<File>[];
readonly workflowConfig: WorkflowConfig<File, FileStatus> = this._configService.workflowConfig(() => this.reloadDossiers());
readonly actionConfigs: readonly ActionConfig[] = this._configService.actionConfig;
@ViewChild(DossierDetailsComponent, { static: false }) private readonly _dossierDetailsComponent: DossierDetailsComponent;
private _lastScrolledIndex: number;
@ViewChild('needsWorkFilterTemplate', { read: TemplateRef, static: true })
private readonly _needsWorkFilterTemplate: TemplateRef<unknown>;
@ViewChild('fileInput') private readonly _fileInput: ElementRef;
@ViewChild(TableComponent) private readonly _tableComponent: TableComponent<Dossier>;
constructor(
private readonly _toaster: Toaster,
protected readonly _injector: Injector,
private readonly _router: Router,
private readonly _userService: UserService,
readonly permissionsService: PermissionsService,
private readonly _loadingService: LoadingService,
private readonly _appStateService: AppStateService,
readonly routerHistoryService: RouterHistoryService,
private readonly _appConfigService: AppConfigService,
private readonly _translateService: TranslateService,
private readonly _dialogService: DossiersDialogService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _fileUploadService: FileUploadService,
private readonly _statusOverlayService: StatusOverlayService,
private readonly _fileDropOverlayService: FileDropOverlayService,
private readonly _dossierAttributesService: DossierAttributesService,
private readonly _fileAttributesService: FileAttributesService,
private readonly _configService: ConfigService
) {
super(_injector);
this._loadEntitiesFromState();
this.fileAttributeConfigs = this._fileAttributesService.getFileAttributeConfig(
this.currentDossier.dossierTemplateId
)?.fileAttributeConfigs;
this.tableColumnConfigs = this._configService.tableConfig(this.displayedAttributes);
}
get checkedRequiredFilters(): NestedFilter[] {
return this.filterService.getGroup('quickFilters')?.filters.filter(f => f.required && f.checked);
}
get checkedNotRequiredFilters(): NestedFilter[] {
return this.filterService.getGroup('quickFilters')?.filters.filter(f => !f.required && f.checked);
}
get displayedInFileListAttributes() {
return this.fileAttributeConfigs?.filter(config => config.displayedInFileList) || [];
}
get displayedAttributes(): IFileAttributeConfig[] {
return this.displayedInFileListAttributes.filter(c => c.displayedInFileList);
}
actionPerformed(action?: string, file?: File) {
this.calculateData();
if (action === 'navigate') {
this._router.navigate([file.routerLink]);
}
}
disabledFn = (fileStatus: File) => fileStatus.excluded;
lastOpenedFn = (fileStatus: File) => fileStatus.lastOpened;
async ngOnInit(): Promise<void> {
this._loadingService.start();
try {
this._fileDropOverlayService.initFileDropHandling();
this.calculateData();
this.addSubscription = timer(0, 7500).subscribe(async () => {
await this._appStateService.reloadActiveDossierFilesIfNecessary();
this._loadEntitiesFromState();
});
this.addSubscription = this._appStateService.fileChanged$.subscribe(() => {
this.calculateData();
});
this.addSubscription = this._appStateService.dossierTemplateChanged$.subscribe(() => {
this.fileAttributeConfigs = this._fileAttributesService.getFileAttributeConfig(
this.currentDossier.dossierTemplateId
)?.fileAttributeConfigs;
});
this.addSubscription = this._tableComponent.scrollViewport.scrolledIndexChange
.pipe(tap(index => (this._lastScrolledIndex = index)))
.subscribe();
this.dossierAttributes = await this._dossierAttributesService.getValues(this.currentDossier);
} catch (e) {
console.log('Error from dossier overview screen: ', e);
} finally {
this._loadingService.stop();
}
}
ngOnDestroy(): void {
this._fileDropOverlayService.cleanupFileDropHandling();
super.ngOnDestroy();
}
async ngOnAttach() {
await this._appStateService.reloadActiveDossierFiles();
this._loadEntitiesFromState();
await this.ngOnInit();
this._tableComponent.scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
}
ngOnDetach() {
this.ngOnDestroy();
}
async reanalyseDossier() {
try {
await this._appStateService.reanalyzeDossier();
await this.reloadDossiers();
this._toaster.success(_('dossier-overview.reanalyse-dossier.success'));
} catch (e) {
this._toaster.error(_('dossier-overview.reanalyse-dossier.error'));
}
}
async reloadDossiers() {
await this._appStateService.getFiles(this.currentDossier, false);
this.calculateData();
}
calculateData(): void {
if (!this._appStateService.activeDossierId) {
return;
}
this._loadEntitiesFromState();
this._computeAllFilters();
this._dossierDetailsComponent?.calculateChartConfig();
this._changeDetectorRef.detectChanges();
}
@HostListener('drop', ['$event'])
onDrop(event: DragEvent): void {
handleFileDrop(event, this.currentDossier, this._uploadFiles.bind(this));
}
@HostListener('dragover', ['$event'])
onDragOver(event): void {
event.stopPropagation();
event.preventDefault();
}
async uploadFiles(files: Files): Promise<void> {
await this._uploadFiles(convertFiles(files, this.currentDossier));
this._fileInput.nativeElement.value = null;
}
async bulkActionPerformed(): Promise<void> {
this.entitiesService.setSelected([]);
await this.reloadDossiers();
}
openAssignDossierMembersDialog(): void {
const data = { dossier: this.currentDossier, section: 'members' };
this._dialogService.openDialog('editDossier', null, data, async () => await this.reloadDossiers());
}
openDossierDictionaryDialog() {
const data = { dossier: this.currentDossier, section: 'dossierDictionary' };
this._dialogService.openDialog('editDossier', null, data, async () => {
await this.reloadDossiers();
});
}
toggleCollapsedDetails() {
this.collapsedDetails = !this.collapsedDetails;
}
recentlyModifiedChecker = (file: File) =>
moment(file.lastUpdated).add(this._appConfigService.values.RECENT_PERIOD_IN_HOURS, 'hours').isAfter(moment());
private _loadEntitiesFromState() {
this.currentDossier = this._appStateService.activeDossier;
if (this.currentDossier) {
this.entitiesService.setEntities(this.currentDossier.files);
}
}
private async _uploadFiles(files: FileUploadModel[]) {
const fileCount = await this._fileUploadService.uploadFiles(files);
if (fileCount) {
this._statusOverlayService.openUploadStatusOverlay();
}
}
private _computeAllFilters() {
if (!this.currentDossier) {
return;
}
const filterGroups = this._configService.filterGroups(
this.entitiesService.all,
this.fileAttributeConfigs,
this._needsWorkFilterTemplate,
() => this.checkedRequiredFilters,
() => this.checkedNotRequiredFilters
);
for (const filterGroup of filterGroups) {
this.filterService.addFilterGroup(filterGroup);
}
}
}

View File

@ -1,19 +1,19 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { Dossier } from '../../../../state/model/dossier';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { AppStateService } from '@state/app-state.service';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { CircleButtonTypes, StatusBarConfig } from '@iqser/common-ui';
import { UserService } from '@services/user.service';
import { AppStateService } from '@state/app-state.service';
import { Dossier } from '@state/model/dossier';
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
@Component({
selector: 'redaction-dossier-listing-actions',
templateUrl: './dossier-listing-actions.component.html',
styleUrls: ['./dossier-listing-actions.component.scss'],
selector: 'redaction-dossiers-listing-actions',
templateUrl: './dossiers-listing-actions.component.html',
styleUrls: ['./dossiers-listing-actions.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DossierListingActionsComponent {
export class DossiersListingActionsComponent {
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
@ -27,20 +27,6 @@ export class DossierListingActionsComponent {
private readonly _userService: UserService
) {}
openEditDossierDialog($event: MouseEvent, dossier: Dossier): void {
this._dialogService.openDialog('editDossier', $event, {
dossier,
afterSave: () => this.actionPerformed.emit()
});
}
reanalyseDossier($event: MouseEvent, dossier: Dossier): void {
$event.stopPropagation();
this.appStateService.reanalyzeDossier(dossier).then(() => {
this.appStateService.loadAllDossiers().then(() => this.actionPerformed.emit());
});
}
get statusConfig(): readonly StatusBarConfig<string>[] {
if (!this.dossier) {
return [];
@ -60,4 +46,18 @@ export class DossierListingActionsComponent {
.sort(StatusSorter.byStatus)
.map(status => ({ length: obj[status], color: status }));
}
openEditDossierDialog($event: MouseEvent, dossier: Dossier): void {
this._dialogService.openDialog('editDossier', $event, {
dossier,
afterSave: () => this.actionPerformed.emit()
});
}
reanalyseDossier($event: MouseEvent, dossier: Dossier): void {
$event.stopPropagation();
this.appStateService.reanalyzeDossier(dossier).then(() => {
this.appStateService.loadAllDossiers().then(() => this.actionPerformed.emit());
});
}
}

View File

@ -4,12 +4,12 @@ import { AppStateService } from '@state/app-state.service';
import { FilterService } from '@iqser/common-ui';
@Component({
selector: 'redaction-dossier-listing-details',
templateUrl: './dossier-listing-details.component.html',
styleUrls: ['./dossier-listing-details.component.scss'],
selector: 'redaction-dossiers-listing-details',
templateUrl: './dossiers-listing-details.component.html',
styleUrls: ['./dossiers-listing-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DossierListingDetailsComponent {
export class DossiersListingDetailsComponent {
@Input() dossiersChartData: DoughnutChartConfig[];
@Input() documentsChartData: DoughnutChartConfig[];

View File

@ -0,0 +1,31 @@
<div [matTooltip]="dossier.dossierName" class="table-item-title heading mb-6" matTooltipPosition="above">
{{ dossier.dossierName }}
</div>
<div class="small-label stats-subtitle mb-6">
<div>
<mat-icon svgIcon="red:template"></mat-icon>
{{ getDossierTemplateNameFor(dossier.dossierTemplateId) }}
</div>
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:document"></mat-icon>
{{ dossier.filesLength }}
</div>
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ dossier.totalNumberOfPages }}
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ dossier.memberIds.length }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ dossier.date | date: 'mediumDate' }}
</div>
<div *ngIf="dossier.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ dossier.dueDate | date: 'mediumDate' }}
</div>
</div>

View File

@ -0,0 +1,18 @@
import { Component, Input } from '@angular/core';
import { Dossier } from '@state/model/dossier';
import { AppStateService } from '@state/app-state.service';
@Component({
selector: 'redaction-dossiers-listing-dossier-name',
templateUrl: './dossiers-listing-dossier-name.component.html',
styleUrls: ['./dossiers-listing-dossier-name.component.scss']
})
export class DossiersListingDossierNameComponent {
@Input() dossier: Dossier;
constructor(private readonly _appStateService: AppStateService) {}
getDossierTemplateNameFor(dossierTemplateId: string): string {
return this._appStateService.getDossierTemplateById(dossierTemplateId).name;
}
}

View File

@ -0,0 +1,12 @@
<div class="cell">
<redaction-dossiers-listing-dossier-name [dossier]="dossier"></redaction-dossiers-listing-dossier-name>
</div>
<div class="cell">
<redaction-needs-work-badge [needsWorkInput]="dossier"></redaction-needs-work-badge>
</div>
<div class="cell user-column">
<redaction-initials-avatar [userId]="dossier.ownerId" [withName]="true"></redaction-initials-avatar>
</div>
<div class="cell status-container">
<redaction-dossiers-listing-actions (actionPerformed)="calculateData.emit()" [dossier]="dossier"></redaction-dossiers-listing-actions>
</div>

View File

@ -0,0 +1,3 @@
.status-container {
width: 160px;
}

View File

@ -0,0 +1,15 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Dossier } from '@state/model/dossier';
import { Required } from '@iqser/common-ui';
@Component({
selector: 'redaction-table-item',
templateUrl: './table-item.component.html',
styleUrls: ['./table-item.component.scss']
})
export class TableItemComponent {
@Input() @Required() dossier!: Dossier;
@Output() readonly calculateData = new EventEmitter<void>();
constructor() {}
}

View File

@ -0,0 +1,213 @@
import { Injectable, TemplateRef } from '@angular/core';
import { IFilterGroup, keyChecker, NestedFilter, TableColumnConfig } from '@iqser/common-ui';
import { Dossier } from '@state/model/dossier';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core';
import { UserPreferenceService } from '@services/user-preference.service';
import { UserService } from '@services/user.service';
import { ButtonConfig } from '@shared/components/page-header/models/button-config.model';
import { User } from '@models/user';
import { fileStatusTranslations } from '../../translations/file-status-translations';
import {
annotationFilterChecker,
dossierMemberChecker,
dossierStatusChecker,
dossierTemplateChecker,
RedactionFilterSorter,
StatusSorter
} from '@utils/index';
import { workloadTranslations } from '../../translations/workload-translations';
import { AppStateService } from '@state/app-state.service';
@Injectable()
export class ConfigService {
constructor(
private readonly _translateService: TranslateService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _userService: UserService,
private readonly _appStateService: AppStateService
) {}
get tableConfig(): TableColumnConfig<Dossier>[] {
return [
{ label: _('dossier-listing.table-col-names.name'), sortByKey: 'searchKey', width: '2fr' },
{ label: _('dossier-listing.table-col-names.needs-work') },
{ label: _('dossier-listing.table-col-names.owner'), class: 'user-column' },
{ label: _('dossier-listing.table-col-names.status'), class: 'flex-end', width: 'auto' }
];
}
get _currentUser(): User {
return this._userService.currentUser;
}
private get _quickFilters(): NestedFilter[] {
const myDossiersLabel = this._translateService.instant('dossier-listing.quick-filters.my-dossiers');
const filters = [
{
id: 'my-dossiers',
label: myDossiersLabel,
checker: (dw: Dossier) => {
console.log(dw.ownerId, this._currentUser.id);
return dw.ownerId === this._currentUser.id;
}
},
{
id: 'to-approve',
label: this._translateService.instant('dossier-listing.quick-filters.to-approve'),
checker: (dw: Dossier) => dw.approverIds.includes(this._currentUser.id)
},
{
id: 'to-review',
label: this._translateService.instant('dossier-listing.quick-filters.to-review'),
checker: (dw: Dossier) => dw.memberIds.includes(this._currentUser.id)
},
{
id: 'other',
label: this._translateService.instant('dossier-listing.quick-filters.other'),
checker: (dw: Dossier) => !dw.memberIds.includes(this._currentUser.id)
}
].map(filter => new NestedFilter(filter));
return filters.filter(f => f.label === myDossiersLabel || this._userPreferenceService.areDevFeaturesEnabled);
}
buttonsConfig(addDossier: () => void): ButtonConfig[] {
return [
{
label: _('dossier-listing.add-new'),
action: addDossier,
hide: !this._currentUser.isManager,
icon: 'red:plus',
type: 'primary'
}
];
}
filterGroups(entities: Dossier[], needsWorkFilterTemplate: TemplateRef<unknown>) {
const allDistinctFileStatus = new Set<string>();
const allDistinctPeople = new Set<string>();
const allDistinctNeedsWork = new Set<string>();
const allDistinctDossierTemplates = new Set<string>();
const filterGroups: IFilterGroup[] = [];
entities?.forEach(entry => {
// all people
entry.memberIds.forEach(f => allDistinctPeople.add(f));
// Needs work
entry.files.forEach(file => {
allDistinctFileStatus.add(file.status);
if (file.analysisRequired) {
allDistinctNeedsWork.add('analysis');
}
if (entry.hintsOnly) {
allDistinctNeedsWork.add('hint');
}
if (entry.hasRedactions) {
allDistinctNeedsWork.add('redaction');
}
if (entry.hasSuggestions) {
allDistinctNeedsWork.add('suggestion');
}
if (entry.hasNone) {
allDistinctNeedsWork.add('none');
}
});
allDistinctDossierTemplates.add(entry.dossierTemplateId);
});
const statusFilters = [...allDistinctFileStatus].map(
status =>
new NestedFilter({
id: status,
label: this._translateService.instant(fileStatusTranslations[status])
})
);
filterGroups.push({
slug: 'statusFilters',
label: this._translateService.instant('filters.status'),
icon: 'red:status',
filters: statusFilters.sort((a, b) => StatusSorter[a.id] - StatusSorter[b.id]),
checker: dossierStatusChecker
});
const peopleFilters = [...allDistinctPeople].map(
userId =>
new NestedFilter({
id: userId,
label: this._userService.getNameForId(userId)
})
);
filterGroups.push({
slug: 'peopleFilters',
label: this._translateService.instant('filters.people'),
icon: 'red:user',
filters: peopleFilters,
checker: dossierMemberChecker
});
const needsWorkFilters = [...allDistinctNeedsWork].map(
type =>
new NestedFilter({
id: type,
label: workloadTranslations[type]
})
);
filterGroups.push({
slug: 'needsWorkFilters',
label: this._translateService.instant('filters.needs-work'),
icon: 'red:needs-work',
filterTemplate: needsWorkFilterTemplate,
filters: needsWorkFilters.sort((a, b) => RedactionFilterSorter[a.id] - RedactionFilterSorter[b.id]),
checker: annotationFilterChecker,
matchAll: true
});
const dossierTemplateFilters = [...allDistinctDossierTemplates].map(
id =>
new NestedFilter({
id: id,
label: this._appStateService.getDossierTemplateById(id).name
})
);
filterGroups.push({
slug: 'dossierTemplateFilters',
label: this._translateService.instant('filters.dossier-templates'),
icon: 'red:template',
hide: dossierTemplateFilters.length <= 1,
filters: dossierTemplateFilters,
checker: dossierTemplateChecker
});
const quickFilters = this._quickFilters;
filterGroups.push({
slug: 'quickFilters',
filters: quickFilters,
checker: (dw: Dossier) => quickFilters.reduce((acc, f) => acc || (f.checked && f.checker(dw)), false)
});
const dossierFilters = entities.map(
dossier =>
new NestedFilter({
id: dossier.dossierName,
label: dossier.dossierName
})
);
filterGroups.push({
slug: 'dossierNameFilter',
label: this._translateService.instant('dossier-listing.filters.label'),
icon: 'red:folder',
filters: dossierFilters,
filterceptionPlaceholder: this._translateService.instant('dossier-listing.filters.search'),
checker: keyChecker('dossierName')
});
return filterGroups;
}
}

View File

@ -0,0 +1,34 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IqserIconsModule } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { DossiersListingScreenComponent } from './screen/dossiers-listing-screen.component';
import { RouterModule } from '@angular/router';
import { DossiersListingActionsComponent } from './components/dossiers-listing-actions/dossiers-listing-actions.component';
import { SharedModule } from '@shared/shared.module';
import { DossiersListingDetailsComponent } from './components/dossiers-listing-details/dossiers-listing-details.component';
import { DossiersListingDossierNameComponent } from './components/dossiers-listing-dossier-name/dossiers-listing-dossier-name.component';
import { ConfigService } from './config.service';
import { TableItemComponent } from './components/table-item/table-item.component';
import { SharedDossiersModule } from '../../shared/shared-dossiers.module';
const routes = [
{
path: '',
component: DossiersListingScreenComponent,
pathMatch: 'full'
}
];
@NgModule({
declarations: [
DossiersListingScreenComponent,
DossiersListingActionsComponent,
DossiersListingDetailsComponent,
DossiersListingDossierNameComponent,
TableItemComponent
],
providers: [ConfigService],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, SharedDossiersModule, IqserIconsModule, TranslateModule]
})
export class DossiersListingModule {}

View File

@ -0,0 +1,36 @@
<section>
<redaction-page-header [buttonConfigs]="buttonConfigs"></redaction-page-header>
<div class="overlay-shadow"></div>
<div class="red-content-inner">
<div class="content-container">
<iqser-table
(noDataAction)="openAddDossierDialog()"
[hasScrollButton]="true"
[itemSize]="85"
[noDataButtonLabel]="'dossier-listing.no-data.action' | translate"
[noDataText]="'dossier-listing.no-data.title' | translate"
[noMatchText]="'dossier-listing.no-match.title' | translate"
[showNoDataButton]="currentUser.isManager"
noDataIcon="red:folder"
></iqser-table>
</div>
<div class="right-container" iqserHasScrollbar>
<redaction-dossiers-listing-details
*ngIf="(entitiesService.noData$ | async) === false"
[documentsChartData]="documentsChartData"
[dossiersChartData]="dossiersChartData"
></redaction-dossiers-listing-details>
</div>
</div>
</section>
<ng-template #needsWorkFilterTemplate let-filter="filter">
<redaction-type-filter [filter]="filter"></redaction-type-filter>
</ng-template>
<ng-template #tableItemTemplate let-dossier="entity">
<redaction-table-item (calculateData)="calculateData()" [dossier]="dossier"></redaction-table-item>
</ng-template>

View File

@ -0,0 +1,14 @@
.right-container {
display: flex;
width: 466px;
min-width: 466px;
padding-right: 11px;
&.has-scrollbar:hover {
padding-right: 0;
}
redaction-dossiers-listing-details {
min-width: 466px;
}
}

View File

@ -0,0 +1,141 @@
import { AfterViewInit, Component, forwardRef, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { DossierStatuses } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { Dossier } from '@state/model/dossier';
import { UserService } from '@services/user.service';
import { PermissionsService } from '@services/permissions.service';
import { TranslateChartService } from '@services/translate-chart.service';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { timer } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Router } from '@angular/router';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { groupBy, OnAttach, OnDetach, StatusSorter } from '@utils/index';
import { DefaultListingServices, ListingComponent, TableComponent } from '@iqser/common-ui';
import { fileStatusTranslations } from '../../../translations/file-status-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ConfigService } from '../config.service';
@Component({
templateUrl: './dossiers-listing-screen.component.html',
styleUrls: ['./dossiers-listing-screen.component.scss'],
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => DossiersListingScreenComponent) }]
})
export class DossiersListingScreenComponent
extends ListingComponent<Dossier>
implements OnInit, AfterViewInit, OnDestroy, OnAttach, OnDetach
{
readonly currentUser = this._userService.currentUser;
readonly tableColumnConfigs = this._configService.tableConfig;
readonly tableHeaderLabel = _('dossier-listing.table-header.title');
readonly buttonConfigs = this._configService.buttonsConfig(() => this.openAddDossierDialog());
dossiersChartData: DoughnutChartConfig[] = [];
documentsChartData: DoughnutChartConfig[] = [];
private _lastScrolledIndex: number;
@ViewChild('needsWorkFilterTemplate', {
read: TemplateRef,
static: true
})
private readonly _needsWorkFilterTemplate: TemplateRef<unknown>;
@ViewChild(TableComponent) private readonly _tableComponent: TableComponent<Dossier>;
constructor(
private readonly _router: Router,
protected readonly _injector: Injector,
private readonly _userService: UserService,
readonly permissionsService: PermissionsService,
private readonly _appStateService: AppStateService,
private readonly _dialogService: DossiersDialogService,
private readonly _translateChartService: TranslateChartService,
private readonly _configService: ConfigService
) {
super(_injector);
this._appStateService.reset();
this._loadEntitiesFromState();
}
private get _activeDossiersCount(): number {
return this.entitiesService.all.filter(p => p.status === DossierStatuses.ACTIVE).length;
}
private get _inactiveDossiersCount(): number {
return this.entitiesService.all.length - this._activeDossiersCount;
}
ngOnInit(): void {
this.calculateData();
this.addSubscription = timer(0, 10000).subscribe(async () => {
await this._appStateService.loadAllDossiers();
this._loadEntitiesFromState();
this.calculateData();
});
this.addSubscription = this._appStateService.fileChanged$.subscribe(() => {
this.calculateData();
});
}
ngAfterViewInit(): void {
this.addSubscription = this._tableComponent.scrollViewport.scrolledIndexChange
.pipe(tap(index => (this._lastScrolledIndex = index)))
.subscribe();
}
ngOnAttach(): void {
this._appStateService.reset();
this._loadEntitiesFromState();
this.ngOnInit();
this.ngAfterViewInit();
this._tableComponent.scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
}
ngOnDetach(): void {
this.ngOnDestroy();
}
openAddDossierDialog(): void {
this._dialogService.openDialog('addDossier', null, null, async addResponse => {
await this._router.navigate([`/main/dossiers/${addResponse.dossier.id}`]);
if (addResponse.addMembers) {
this._dialogService.openDialog('editDossier', null, {
dossier: addResponse.dossier,
section: 'members'
});
}
});
}
calculateData(): void {
this._computeAllFilters();
this.dossiersChartData = [
{ value: this._activeDossiersCount, color: 'ACTIVE', label: _('active') },
{ value: this._inactiveDossiersCount, color: 'DELETED', label: _('archived') }
];
const groups = groupBy(this._appStateService.aggregatedFiles, 'status');
this.documentsChartData = [];
for (const status of Object.keys(groups)) {
this.documentsChartData.push({
value: groups[status].length,
color: status,
label: fileStatusTranslations[status],
key: status
});
}
this.documentsChartData.sort(StatusSorter.byStatus);
this.documentsChartData = this._translateChartService.translateStatus(this.documentsChartData);
}
private _loadEntitiesFromState() {
this.entitiesService.setEntities(this._appStateService.allDossiers);
}
private _computeAllFilters() {
const filterGroups = this._configService.filterGroups(this.entitiesService.all, this._needsWorkFilterTemplate);
for (const filterGroup of filterGroups) {
this.filterService.addFilterGroup(filterGroup);
}
}
}

View File

@ -18,7 +18,7 @@ import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ManualAnnotationResponse } from '@models/file/manual-annotation-response';
import { AnnotationData, FileDataModel } from '@models/file/file-data.model';
import { FileActionService } from '../../services/file-action.service';
import { FileActionService } from '../../shared/services/file-action.service';
import { AnnotationDrawService } from '../../services/annotation-draw.service';
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
import { File } from '@models/file/file';
@ -38,7 +38,7 @@ import { TranslateService } from '@ngx-translate/core';
import { fileStatusTranslations } from '../../translations/file-status-translations';
import { handleFilterDelta } from '@utils/filter-utils';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FileActionsComponent } from '../../components/file-actions/file-actions.component';
import { FileActionsComponent } from '../../shared/components/file-actions/file-actions.component';
import { User } from '@models/user';
import { FilesService } from '../../services/files.service';
import Annotation = Core.Annotations.Annotation;

View File

@ -2,8 +2,7 @@ import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, S
import { PermissionsService } from '@services/permissions.service';
import { File } from '@models/file/file';
import { AppStateService } from '@state/app-state.service';
import { FileActionService } from '../../services/file-action.service';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { ConfirmationDialogInput } from '@shared/dialogs/confirmation-dialog/confirmation-dialog.component';
import { AutoUnsubscribe, CircleButtonType, CircleButtonTypes, LoadingService, Required, StatusBarConfig, Toaster } from '@iqser/common-ui';
import { FileManagementControllerService, FileStatus } from '@redaction/red-ui-http';
@ -12,6 +11,7 @@ import { UserService } from '@services/user.service';
import { filter } from 'rxjs/operators';
import { UserPreferenceService } from '@services/user-preference.service';
import { LongPressEvent } from '@shared/directives/long-press.directive';
import { FileActionService } from '../../services/file-action.service';
@Component({
selector: 'redaction-file-actions',

View File

@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { File } from '@models/file/file';
import { Dossier } from '../../../../state/model/dossier';
import { Dossier } from '@state/model/dossier';
@Component({
selector: 'redaction-needs-work-badge',

View File

@ -4,10 +4,10 @@ import { UserService } from '@services/user.service';
import { ReanalysisControllerService } from '@redaction/red-ui-http';
import { File } from '@models/file/file';
import { PermissionsService } from '@services/permissions.service';
import { DossiersDialogService } from './dossiers-dialog.service';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { ConfirmationDialogInput } from '@shared/dialogs/confirmation-dialog/confirmation-dialog.component';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FilesService } from './files.service';
import { FilesService } from '../../services/files.service';
@Injectable()
export class FileActionService {

View File

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FileActionService } from './services/file-action.service';
import { FileActionsComponent } from './components/file-actions/file-actions.component';
import { NeedsWorkBadgeComponent } from './components/needs-work-badge/needs-work-badge.component';
import { IqserIconsModule } from '@iqser/common-ui';
import { SharedModule } from '@shared/shared.module';
const components = [FileActionsComponent, NeedsWorkBadgeComponent];
@NgModule({
declarations: [...components],
exports: [...components],
providers: [FileActionService],
imports: [CommonModule, IqserIconsModule, SharedModule]
})
export class SharedDossiersModule {}

View File

@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { AppStateService } from '../../../../state/app-state.service';
import { INestedFilter } from '@iqser/common-ui';
@Component({

View File

@ -19,13 +19,15 @@ import { MomentDateAdapter } from '@angular/material-moment-adapter';
import { SelectComponent } from './components/select/select.component';
import { NavigateLastDossiersScreenDirective } from './directives/navigate-last-dossiers-screen.directive';
import { DictionaryManagerComponent } from './components/dictionary-manager/dictionary-manager.component';
import { SideNavComponent } from '@shared/components/side-nav/side-nav.component';
import { SideNavComponent } from './components/side-nav/side-nav.component';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { AssignUserDropdownComponent } from './components/assign-user-dropdown/assign-user-dropdown.component';
import { PageHeaderComponent } from './components/page-header/page-header.component';
import { DatePipe } from '@shared/pipes/date.pipe';
import { LongPressDirective } from '@shared/directives/long-press.directive';
import { NamePipe } from '@shared/pipes/name.pipe';
import { DatePipe } from './pipes/date.pipe';
import { LongPressDirective } from './directives/long-press.directive';
import { NamePipe } from './pipes/name.pipe';
import { TypeFilterComponent } from './components/type-filter/type-filter.component';
import { TeamMembersComponent } from './components/team-members/team-members.component';
const buttons = [FileDownloadBtnComponent, UserButtonComponent];
@ -42,6 +44,8 @@ const components = [
DictionaryManagerComponent,
AssignUserDropdownComponent,
PageHeaderComponent,
TypeFilterComponent,
TeamMembersComponent,
...buttons
];

View File

@ -0,0 +1,17 @@
export * from './sorters/redaction-filter-sorter';
export * from './sorters/status-sorter';
export * from './sorters/super-type-sorter';
export * from './api-path-interceptor';
export * from './configuration.initializer';
export * from './custom-route-reuse.strategy';
export * from './date-inputs-utils';
export * from './file-download-utils';
export * from './file-drop-utils';
export * from './filter-utils';
export * from './functions';
export * from './global-error-handler.service';
export * from './missing-translations-handler';
export * from './page-stamper';
export * from './pdf-coordinates';
export * from './pruning-translation-loader';

@ -1 +1 @@
Subproject commit 54eb8173cf7ce35f29f4e9df309954bbd1cf7a5c
Subproject commit 85d22fbcc9b3673d11dbf542e3b6ee16c7791d84