import { Injectable, inject } from '@angular/core' import { Observable } from 'rxjs' import { map } from 'rxjs/operators' import { AuditLogEntry } from 'src/app/data/auditlog-entry' import { CustomField } from 'src/app/data/custom-field' import { DOCUMENT_SORT_FIELDS, DOCUMENT_SORT_FIELDS_FULLTEXT, Document, DocumentVersionInfo, } from 'src/app/data/document' import { DocumentMetadata } from 'src/app/data/document-metadata' import { DocumentSuggestions } from 'src/app/data/document-suggestions' import { FilterRule } from 'src/app/data/filter-rule' import { Results } from 'src/app/data/results' import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { queryParamsFromFilterRules } from '../../utils/query-params' import { PermissionAction, PermissionType, PermissionsService, } from '../permissions.service' import { SettingsService } from '../settings.service' import { AbstractPaperlessService } from './abstract-paperless-service' import { CustomFieldsService } from './custom-fields.service' export interface SelectionDataItem { id: number document_count: number } export interface SelectionData { selected_storage_paths: SelectionDataItem[] selected_correspondents: SelectionDataItem[] selected_tags: SelectionDataItem[] selected_document_types: SelectionDataItem[] selected_custom_fields: SelectionDataItem[] } export enum BulkEditSourceMode { LATEST_VERSION = 'latest_version', EXPLICIT_SELECTION = 'explicit_selection', } export type DocumentBulkEditMethod = | 'set_correspondent' | 'set_document_type' | 'set_storage_path' | 'add_tag' | 'remove_tag' | 'modify_tags' | 'modify_custom_fields' | 'set_permissions' export interface MergeDocumentsRequest { metadata_document_id?: number delete_originals?: boolean archive_fallback?: boolean source_mode?: BulkEditSourceMode } export interface EditPdfOperation { page: number rotate?: number doc?: number } export interface EditPdfDocumentsRequest { operations: EditPdfOperation[] delete_original?: boolean update_document?: boolean include_metadata?: boolean source_mode?: BulkEditSourceMode } export interface RemovePasswordDocumentsRequest { password: string update_document?: boolean delete_original?: boolean include_metadata?: boolean source_mode?: BulkEditSourceMode } @Injectable({ providedIn: 'root', }) export class DocumentService extends AbstractPaperlessService { private permissionsService = inject(PermissionsService) private settingsService = inject(SettingsService) private customFieldService = inject(CustomFieldsService) private _searchQuery: string private _sortFields get sortFields() { return this._sortFields } private _sortFieldsFullText get sortFieldsFullText() { return this._sortFieldsFullText } private customFields: CustomField[] = [] constructor() { super() this.resourceName = 'documents' this.reload() } public reload() { if ( this.permissionsService.currentUserCan( PermissionAction.View, PermissionType.CustomField ) ) { this.customFieldService.listAll().subscribe((fields) => { this.customFields = fields.results this.setupSortFields() }) } this.setupSortFields() } private setupSortFields() { this._sortFields = [...DOCUMENT_SORT_FIELDS] if ( this.permissionsService.currentUserCan( PermissionAction.View, PermissionType.CustomField ) ) { this.customFields.forEach((field) => { this._sortFields.push({ field: `custom_field_${field.id}`, name: field.name, }) }) } let excludes = [] if ( !this.permissionsService.currentUserCan( PermissionAction.View, PermissionType.Correspondent ) ) { excludes.push('correspondent__name') } if ( !this.permissionsService.currentUserCan( PermissionAction.View, PermissionType.DocumentType ) ) { excludes.push('document_type__name') } if ( !this.permissionsService.currentUserCan( PermissionAction.View, PermissionType.User ) ) { excludes.push('owner') } if (!this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED)) { excludes.push('num_notes') } this._sortFields = this._sortFields.filter( (field) => !excludes.includes(field.field) ) this._sortFieldsFullText = [ ...this._sortFields, ...DOCUMENT_SORT_FIELDS_FULLTEXT, ] } listFiltered( page?: number, pageSize?: number, sortField?: string, sortReverse?: boolean, filterRules?: FilterRule[], extraParams = {} ): Observable> { return this.list( page, pageSize, sortField, sortReverse, Object.assign(extraParams, queryParamsFromFilterRules(filterRules)) ) } listAllFilteredIds(filterRules?: FilterRule[]): Observable { return this.listFiltered(1, 100000, null, null, filterRules, { fields: 'id', }).pipe(map((response) => response.results.map((doc) => doc.id))) } get( id: number, versionID: number = null, fields: string = null ): Observable { const params: { full_perms: boolean; version?: string; fields?: string } = { full_perms: true, } if (versionID) { params.version = versionID.toString() } if (fields) { params.fields = fields } return this.http.get(this.getResourceUrl(id), { params, }) } getPreviewUrl( id: number, original: boolean = false, versionID: number = null ): string { let url = new URL(this.getResourceUrl(id, 'preview')) if (this._searchQuery) url.hash = `#search="${this.searchQuery}"` if (original) { url.searchParams.append('original', 'true') } if (versionID) { url.searchParams.append('version', versionID.toString()) } return url.toString() } getThumbUrl(id: number, versionID: number = null): string { let url = new URL(this.getResourceUrl(id, 'thumb')) if (versionID) { url.searchParams.append('version', versionID.toString()) } return url.toString() } getDownloadUrl( id: number, original: boolean = false, versionID: number = null, followFormatting: boolean = false ): string { let url = new URL(this.getResourceUrl(id, 'download')) if (original) { url.searchParams.append('original', 'true') } if (versionID) { url.searchParams.append('version', versionID.toString()) } if (followFormatting) { url.searchParams.append('follow_formatting', 'true') } return url.toString() } uploadVersion(documentId: number, file: File, versionLabel?: string) { const formData = new FormData() formData.append('document', file, file.name) if (versionLabel) { formData.append('version_label', versionLabel) } return this.http.post( this.getResourceUrl(documentId, 'update_version'), formData ) } getVersions(documentId: number): Observable { return this.http.get(this.getResourceUrl(documentId), { params: { fields: 'id,versions', }, }) } getRootId(documentId: number) { return this.http.get<{ root_id: number }>( this.getResourceUrl(documentId, 'root') ) } deleteVersion(rootDocumentId: number, versionId: number) { return this.http.delete<{ result: string; current_version_id: number }>( this.getResourceUrl(rootDocumentId, `versions/${versionId}`) ) } updateVersionLabel( rootDocumentId: number, versionId: number, versionLabel: string | null ): Observable { return this.http.patch( this.getResourceUrl(rootDocumentId, `versions/${versionId}`), { version_label: versionLabel } ) } getNextAsn(): Observable { return this.http.get(this.getResourceUrl(null, 'next_asn')) } patch(o: Document, versionID: number = null): Observable { o.remove_inbox_tags = !!this.settingsService.get( SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS ) this.clearCache() return this.http.patch(this.getResourceUrl(o.id), o, { params: versionID ? { version: versionID.toString() } : {}, }) } uploadDocument(formData) { return this.http.post( this.getResourceUrl(null, 'post_document'), formData, { reportProgress: true, observe: 'events' } ) } getMetadata( id: number, versionID: number = null ): Observable { let url = new URL(this.getResourceUrl(id, 'metadata')) if (versionID) { url.searchParams.append('version', versionID.toString()) } return this.http.get(url.toString()) } bulkEdit(ids: number[], method: DocumentBulkEditMethod, args: any) { return this.http.post(this.getResourceUrl(null, 'bulk_edit'), { documents: ids, method: method, parameters: args, }) } deleteDocuments(ids: number[]) { return this.http.post(this.getResourceUrl(null, 'delete'), { documents: ids, }) } reprocessDocuments(ids: number[]) { return this.http.post(this.getResourceUrl(null, 'reprocess'), { documents: ids, }) } rotateDocuments( ids: number[], degrees: number, sourceMode: BulkEditSourceMode = BulkEditSourceMode.LATEST_VERSION ) { return this.http.post(this.getResourceUrl(null, 'rotate'), { documents: ids, degrees, source_mode: sourceMode, }) } mergeDocuments(ids: number[], request: MergeDocumentsRequest = {}) { return this.http.post(this.getResourceUrl(null, 'merge'), { documents: ids, ...request, }) } editPdfDocuments(ids: number[], request: EditPdfDocumentsRequest) { return this.http.post(this.getResourceUrl(null, 'edit_pdf'), { documents: ids, ...request, }) } removePasswordDocuments( ids: number[], request: RemovePasswordDocumentsRequest ) { return this.http.post(this.getResourceUrl(null, 'remove_password'), { documents: ids, ...request, }) } getSelectionData(ids: number[]): Observable { return this.http.post( this.getResourceUrl(null, 'selection_data'), { documents: ids } ) } getSuggestions(id: number): Observable { return this.http.get( this.getResourceUrl(id, 'suggestions') ) } getHistory(id: number): Observable { return this.http.get(this.getResourceUrl(id, 'history')) } bulkDownload( ids: number[], content = 'both', useFilenameFormatting: boolean = false ) { return this.http.post( this.getResourceUrl(null, 'bulk_download'), { documents: ids, content: content, follow_formatting: useFilenameFormatting, }, { responseType: 'blob' } ) } public set searchQuery(query: string) { this._searchQuery = query.trim() } public get searchQuery(): string { return this._searchQuery } emailDocuments( documentIds: number[], addresses: string, subject: string, message: string, useArchiveVersion: boolean ): Observable { return this.http.post(this.getResourceUrl(null, 'email'), { documents: documentIds, addresses: addresses, subject: subject, message: message, use_archive_version: useArchiveVersion, }) } }