@dignite/vault-extract 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/fesm2022/dignite-vault-extract-config.mjs +82 -0
- package/fesm2022/dignite-vault-extract-config.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-cabinet-list.component-Ch0gpSCc.mjs +184 -0
- package/fesm2022/dignite-vault-extract-documents-cabinet-list.component-Ch0gpSCc.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-content-type-DjCs-s4E.mjs +115 -0
- package/fesm2022/dignite-vault-extract-documents-content-type-DjCs-s4E.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-document-detail.component-DHs42DWJ.mjs +1146 -0
- package/fesm2022/dignite-vault-extract-documents-document-detail.component-DHs42DWJ.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-document-file-preview.component-CStXf8v9.mjs +72 -0
- package/fesm2022/dignite-vault-extract-documents-document-file-preview.component-CStXf8v9.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-document-list.component-jThR5cct.mjs +642 -0
- package/fesm2022/dignite-vault-extract-documents-document-list.component-jThR5cct.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-document-overview.component-BHUUUIVr.mjs +318 -0
- package/fesm2022/dignite-vault-extract-documents-document-overview.component-BHUUUIVr.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-document-recycle-bin.component-dqeBrw22.mjs +178 -0
- package/fesm2022/dignite-vault-extract-documents-document-recycle-bin.component-dqeBrw22.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-document-type-list.component-C8kXFJGb.mjs +464 -0
- package/fesm2022/dignite-vault-extract-documents-document-type-list.component-C8kXFJGb.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-export-template-list.component-DlmZFFF1.mjs +361 -0
- package/fesm2022/dignite-vault-extract-documents-export-template-list.component-DlmZFFF1.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-extensible-table-DkLXuoWo.mjs +53 -0
- package/fesm2022/dignite-vault-extract-documents-extensible-table-DkLXuoWo.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-field-definition-list.component-ClmWkRun.mjs +530 -0
- package/fesm2022/dignite-vault-extract-documents-field-definition-list.component-ClmWkRun.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-field-reextraction-modal.component-D7OOycv9.mjs +163 -0
- package/fesm2022/dignite-vault-extract-documents-field-reextraction-modal.component-D7OOycv9.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-format-bytes-Cd3QwfQZ.mjs +19 -0
- package/fesm2022/dignite-vault-extract-documents-format-bytes-Cd3QwfQZ.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-format-field-value-Xjb8lwzA.mjs +22 -0
- package/fesm2022/dignite-vault-extract-documents-format-field-value-Xjb8lwzA.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents.mjs +71 -0
- package/fesm2022/dignite-vault-extract-documents.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract.mjs +522 -0
- package/fesm2022/dignite-vault-extract.mjs.map +1 -0
- package/package.json +38 -0
- package/types/dignite-vault-extract-config.d.ts +5 -0
- package/types/dignite-vault-extract-documents.d.ts +5 -0
- package/types/dignite-vault-extract.d.ts +521 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dignite-vault-extract-documents-document-list.component-jThR5cct.mjs","sources":["../../../packages/vault-extract/documents/src/lib/documents/document-list/document-list.component.ts","../../../packages/vault-extract/documents/src/lib/documents/document-list/document-list.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n LOCALE_ID,\n OnInit,\n computed,\n inject,\n signal,\n} from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { CommonModule, formatDate } from '@angular/common';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { FormsModule } from '@angular/forms';\nimport {\n ListService,\n LocalizationPipe,\n LocalizationService,\n PermissionService,\n escapeHtmlChars,\n} from '@abp/ng.core';\nimport {\n EntityProp,\n EXTENSIONS_IDENTIFIER,\n ExtensionsService,\n ExtensibleTableComponent,\n ePropType,\n} from '@abp/ng.components/extensible';\nimport { ConfirmationService, ToasterService } from '@abp/ng.theme.shared';\nimport { Confirmation } from '@abp/ng.theme.shared';\nimport { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';\nimport { of } from 'rxjs';\nimport {\n CabinetDto,\n CabinetService,\n DocumentLifecycleStatus,\n DocumentListItemDto,\n DocumentReviewReasons,\n DocumentService,\n DocumentStatisticsService,\n DocumentTypeDto,\n DocumentTypeService,\n FieldDefinitionDto,\n FieldDefinitionService,\n GetDocumentListInput,\n EXTRACT_PERMISSIONS,\n} from '@dignite/vault-extract';\nimport { ClientPagedResult, configureEntityTable, EXTRACT_TABLES } from '../../shared/extensible-table';\nimport { formatExtractedFieldValue } from '../../shared/format-field-value';\n\ninterface TableActivateEvent {\n type?: string;\n row?: DocumentListItemDto;\n}\n\n@Component({\n selector: 'lib-document-list',\n templateUrl: './document-list.component.html',\n styleUrls: ['./document-list.component.scss'],\n imports: [\n CommonModule,\n FormsModule,\n LocalizationPipe,\n ExtensibleTableComponent,\n NgbDropdownModule,\n ],\n providers: [\n ListService,\n {\n provide: EXTENSIONS_IDENTIFIER,\n useValue: EXTRACT_TABLES.Documents,\n },\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DocumentListComponent implements OnInit {\n private readonly documentService = inject(DocumentService);\n private readonly statisticsService = inject(DocumentStatisticsService);\n private readonly documentTypeService = inject(DocumentTypeService);\n private readonly fieldDefinitionService = inject(FieldDefinitionService);\n private readonly cabinetService = inject(CabinetService);\n private readonly router = inject(Router);\n private readonly route = inject(ActivatedRoute);\n private readonly confirmation = inject(ConfirmationService);\n private readonly toaster = inject(ToasterService);\n private readonly permissionService = inject(PermissionService);\n private readonly destroyRef = inject(DestroyRef);\n private readonly extensions = inject(ExtensionsService);\n private readonly locale = inject(LOCALE_ID);\n\n readonly list = inject(ListService);\n\n readonly canDelete = this.permissionService.getGrantedPolicy(\n EXTRACT_PERMISSIONS.Documents.Delete,\n );\n readonly canConfirm = this.permissionService.getGrantedPolicy(\n EXTRACT_PERMISSIONS.Documents.ConfirmClassification,\n );\n readonly canUpload = this.permissionService.getGrantedPolicy(\n EXTRACT_PERMISSIONS.Documents.Upload,\n );\n readonly canViewCabinets = this.permissionService.getGrantedPolicy(\n EXTRACT_PERMISSIONS.Cabinets.Default,\n );\n readonly hasDocumentActions = this.canConfirm || this.canDelete;\n\n documents = signal<ClientPagedResult<DocumentListItemDto>>({ totalCount: 0, items: [] });\n isLoading = signal(true);\n // Bumped whenever the dynamic columns change. ExtensibleTableComponent snapshots its\n // column list at construction, so the template keys the table on this value (@for …\n // track key) to force a fresh instance — deterministic, no setTimeout/flicker.\n tableKey = signal(0);\n\n typeFilter = signal<string>('');\n cabinetFilter = signal<string>('');\n lifecycleFilter = signal<DocumentLifecycleStatus | undefined>(undefined);\n // #395: needs-review filter. Replaces the standalone review-queue page — when on, the list shows only\n // documents that still require operator attention (hasReviewReasons, the queue's RequiresAttention\n // predicate). Seeded from the ?review=1 deep-link used by the overview needs-review entry points.\n reviewFilter = signal<boolean>(false);\n // #354: when set, the list shows only the sub-documents derived from this source document (a container's\n // children). subDocumentsParent is the container itself (for the indicator banner); it is null when the filter\n // was seeded from a deep-link query param and the parent row is not in hand.\n originDocumentIdFilter = signal<string>('');\n subDocumentsParent = signal<DocumentListItemDto | null>(null);\n confirmingDoc = signal<DocumentListItemDto | null>(null);\n documentTypes = signal<DocumentTypeDto[]>([]);\n cabinets = signal<CabinetDto[]>([]);\n // Dynamic ExtractedFields columns — populated only while a single documentTypeCode\n // filter is active (then the page shares one field schema). Empty for no-type /\n // mixed-type views, so the columns disappear. Driven off the type's field\n // definitions (not the union of extractedFields keys) so headers stay stable and\n // friendly even for fields no document in the page happened to fill.\n extractedFieldColumns = signal<FieldDefinitionDto[]>([]);\n selectedTypeId = signal('');\n isConfirming = signal(false);\n\n // #284 review-queue gateway: the toolbar badge shows the canonical needs-review total for the current\n // layer (DocumentStatisticsDto.NeedsReviewCount — same RequiresAttention predicate the review queue runs,\n // #333), not a page-local count, so it stays correct across pagination and once the list is unfiltered.\n reviewQueueCount = signal(0);\n\n // #354: render the row actions column when the user has confirm/delete actions OR any row is a container\n // (exposes \"view sub-documents\") OR any row is a sub-document (exposes \"view parent / view siblings\") —\n // these provenance actions are available regardless of confirm/delete permissions.\n readonly showActionsColumn = computed(\n () => this.hasDocumentActions || this.documents().items.some(d => d.isContainer || d.originDocumentId),\n );\n\n readonly DocumentLifecycleStatus = DocumentLifecycleStatus;\n readonly DocumentReviewReasons = DocumentReviewReasons;\n\n constructor() {\n this.rebuildTableProps([]);\n }\n\n ngOnInit(): void {\n // Seed filters from query params first so the overview cards (#335) can deep-link into a\n // cabinet- or type-filtered list before the initial load runs.\n this.applyQueryParamFilters();\n // Seed page + sorting into the ListService BEFORE hookToQuery so the initial fetch uses them (the\n // query$ pipe debounces, so these synchronous seeds coalesce with the constructor's default into a\n // single request). This is what makes Back restore the exact page/sort the operator left, not just the\n // filters.\n this.applyQueryParamPaging();\n this.hookListQuery();\n // Review-queue badge total — only fetched/shown for operators who can open the queue (#284).\n this.loadReviewQueueCount();\n // Document types drive the type filter, the dynamic extracted-field columns, and\n // the confirm-classification picker. Every Documents.Default user needs them, and\n // the read is now decoupled from schema-admin permission (#223 — GetVisible no longer\n // requires DocumentTypes.Default), so load unconditionally; the error fallback keeps\n // the list usable if it ever 403s.\n this.loadDocumentTypes();\n // Cabinet getList is gated by Cabinets.Default; only fetch when granted to\n // avoid a 403 for users without cabinet access (cabinet filter/labels hidden).\n if (this.canViewCabinets) {\n this.loadCabinets();\n }\n }\n\n refresh(): void {\n this.list.getWithoutPageReset();\n }\n\n // Overview deep-links (#335) carry cabinetId / documentTypeCode in the URL, and every filter change now\n // writes the active filter set back via writeFiltersToUrl(). Seed the matching filter signals once on\n // load; the top dropdowns are bound to the same signals so they reflect the applied value, and\n // loadDocumentTypes() picks up a seeded typeFilter to load its field columns. Because the filters live in\n // the URL, opening a document and navigating Back restores the exact filtered view instead of resetting\n // to the unfiltered default.\n private applyQueryParamFilters(): void {\n const params = this.route.snapshot.queryParamMap;\n const cabinetId = params.get('cabinetId');\n if (cabinetId) {\n this.cabinetFilter.set(cabinetId);\n }\n const typeCode = params.get('documentTypeCode');\n if (typeCode) {\n this.typeFilter.set(typeCode);\n }\n const lifecycleStatus = params.get('lifecycleStatus');\n if (lifecycleStatus) {\n const parsed = Number(lifecycleStatus);\n if (!Number.isNaN(parsed)) {\n this.lifecycleFilter.set(parsed as DocumentLifecycleStatus);\n }\n }\n // #354: deep-link into a container's sub-documents (the parent row may not be loaded, so the banner falls\n // back to showing the id until/unless the operator navigated via the in-list \"view sub-documents\" action).\n const originDocumentId = params.get('originDocumentId');\n if (originDocumentId) {\n this.originDocumentIdFilter.set(originDocumentId);\n }\n // #395: overview needs-review entry points deep-link here with ?review=1.\n if (params.get('review')) {\n this.reviewFilter.set(true);\n }\n }\n\n // Seed the ListService's page + sorting from the URL. ABP's ListService has no built-in URL binding, but\n // page / sortKey / sortOrder are public read/write, so writing them here makes the first fetch (and the\n // restored view after Back) start on the right page and ordering. The header sort arrow is owned by the\n // table and not re-seeded, so only the data ordering is restored — acceptable, and the only sortable\n // column is the default creationTime.\n private applyQueryParamPaging(): void {\n const params = this.route.snapshot.queryParamMap;\n const page = Number(params.get('page'));\n if (Number.isInteger(page) && page > 0) {\n this.list.page = page;\n }\n const sorting = params.get('sorting');\n if (sorting) {\n const [key, order] = sorting.split(' ');\n if (key && (order === 'asc' || order === 'desc')) {\n this.list.sortKey = key;\n this.list.sortOrder = order;\n }\n }\n }\n\n onLifecycleFilterChange(value: DocumentLifecycleStatus | undefined): void {\n this.lifecycleFilter.set(value);\n this.refreshListFromFirstPage();\n }\n\n onTypeFilterChange(value: string): void {\n this.typeFilter.set(value);\n this.updateExtractedFieldColumns([]);\n if (value) {\n this.loadExtractedFieldColumns(value);\n }\n this.refreshListFromFirstPage();\n }\n\n onCabinetFilterChange(value: string): void {\n this.cabinetFilter.set(value);\n this.refreshListFromFirstPage();\n }\n\n // #395: toggle the needs-review filter (the former review-queue gateway). Refreshes from page 1 so the\n // filtered count and pagination stay consistent.\n toggleReviewFilter(): void {\n this.reviewFilter.update(on => !on);\n this.refreshListFromFirstPage();\n }\n\n // #354: focus the list on a container's sub-documents (those whose OriginDocumentId is this container).\n viewSubDocuments(doc: DocumentListItemDto, event?: Event): void {\n event?.stopPropagation();\n if (!doc.id) {\n return;\n }\n this.subDocumentsParent.set(doc);\n this.originDocumentIdFilter.set(doc.id);\n this.refreshListFromFirstPage();\n }\n\n clearSubDocumentsFilter(): void {\n this.subDocumentsParent.set(null);\n this.originDocumentIdFilter.set('');\n this.refreshListFromFirstPage();\n }\n\n // #354: from a sub-document row, open its source (container) document.\n openParentDocument(doc: DocumentListItemDto, event?: Event): void {\n event?.stopPropagation();\n if (!doc.originDocumentId) {\n return;\n }\n this.router.navigate(['/documents', doc.originDocumentId]);\n }\n\n // #354: from a sub-document row, focus the list on its siblings (all sub-documents of the same source,\n // including this one). The parent row may not be in hand, so the banner falls back to showing the id.\n viewSiblingDocuments(doc: DocumentListItemDto, event?: Event): void {\n event?.stopPropagation();\n if (!doc.originDocumentId) {\n return;\n }\n this.subDocumentsParent.set(null);\n this.originDocumentIdFilter.set(doc.originDocumentId);\n this.refreshListFromFirstPage();\n }\n\n private hookListQuery(): void {\n this.list.requestStatus$\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(status => {\n if (status === 'idle' && this.isLoading() && this.documents().items.length === 0) return;\n this.isLoading.set(status === 'loading');\n });\n\n // query$ is the single funnel for every page / sort / filter change (the table writes page+sort here\n // directly; our filter handlers reach it via refreshListFromFirstPage). Persist the full view state to\n // the URL on each emit so table-driven pagination and sorting survive a round-trip to a document and\n // Back, alongside the filters. (It is debounced inside ListService, so this does not fire per keystroke.)\n this.list.query$\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(() => this.writeStateToUrl());\n\n this.list\n .hookToQuery(query =>\n this.documentService.getList({\n ...this.buildFilter(),\n maxResultCount: query.maxResultCount,\n skipCount: query.skipCount,\n sorting: query.sorting || 'creationTime desc',\n }),\n )\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(result => {\n this.documents.set({\n totalCount: result.totalCount ?? 0,\n items: result.items ?? [],\n });\n });\n }\n\n private refreshListFromFirstPage(): void {\n // Reset to page 1 first (the setter triggers the refetch), then persist — so the URL records page 0,\n // not the page the operator was on before changing the filter. The debounced query$ subscription will\n // also fire and re-persist the same state; writing here too keeps the URL correct synchronously, so a\n // filter-then-immediately-open-a-document sequence still records the new filters before navigating away.\n if (this.list.page === 0) {\n this.list.get();\n } else {\n this.list.page = 0;\n }\n this.writeStateToUrl();\n }\n\n // Mirror the active filters AND the ListService's page/sorting into the URL query string. replaceUrl keeps\n // rapid changes from piling up Back-button steps (each replaces the current /documents/list entry rather\n // than pushing a new one); null values are dropped by the merge handling, so cleared filters and the\n // default page/sort leave a clean URL. applyQueryParamFilters + applyQueryParamPaging re-seed from here on\n // Back, restoring the exact view.\n private writeStateToUrl(): void {\n const sorting = this.list.sortOrder\n ? `${this.list.sortKey} ${this.list.sortOrder}`\n : null;\n this.router.navigate([], {\n relativeTo: this.route,\n queryParams: {\n lifecycleStatus: this.lifecycleFilter() ?? null,\n documentTypeCode: this.typeFilter() || null,\n cabinetId: this.cabinetFilter() || null,\n originDocumentId: this.originDocumentIdFilter() || null,\n review: this.reviewFilter() ? 1 : null,\n page: this.list.page > 0 ? this.list.page : null,\n sorting,\n },\n queryParamsHandling: 'merge',\n replaceUrl: true,\n });\n }\n\n private buildFilter(): Partial<GetDocumentListInput> {\n return {\n documentTypeCode: this.typeFilter() || undefined,\n cabinetId: this.cabinetFilter() || undefined,\n originDocumentId: this.originDocumentIdFilter() || undefined,\n lifecycleStatus: this.lifecycleFilter(),\n // #395: same RequiresAttention predicate the old review queue ran.\n hasReviewReasons: this.reviewFilter() || undefined,\n };\n }\n\n private rebuildTableProps(fields: FieldDefinitionDto[] = this.extractedFieldColumns()): void {\n configureEntityTable<DocumentListItemDto>(\n this.extensions,\n EXTRACT_TABLES.Documents,\n this.createTableProps(fields),\n );\n // Force a fresh ExtensibleTableComponent so it re-reads the just-configured columns\n // (it snapshots its column list at construction). The @for key swap recreates it\n // synchronously within the same change-detection pass.\n this.tableKey.update(v => v + 1);\n }\n\n private createTableProps(fields: FieldDefinitionDto[]): EntityProp<DocumentListItemDto>[] {\n return [\n EntityProp.create<DocumentListItemDto>({\n type: ePropType.String,\n name: 'fileName',\n displayName: '::Document:FileName',\n sortable: false,\n columnWidth: 340,\n valueResolver: data => {\n const doc = data.record;\n const localization = data.getInjected(LocalizationService);\n const fileName = doc.title || doc.fileOrigin?.originalFileName || '-';\n const iconClass = this.isImage(doc)\n ? 'fas fa-file-image fa-lg text-primary'\n : 'fas fa-file-pdf fa-lg text-danger';\n // #350: a container is a bundle of sub-documents and is not itself a business record. Flag it\n // with a badge so operators don't mistake it for a normal document. isContainer is a\n // system-controlled signal carried on the list DTO. #354: the row's \"view sub-documents\" action\n // (containers only) drills into its children via the originDocumentId filter.\n const bundleBadge = doc.isContainer\n ? ` <span class=\"badge bg-dark\">${escapeHtmlChars(localization.instant('::Document:Bundle'))}</span>`\n : '';\n // #354: mirror of the container Bundle badge on the child side — a sub-document carries\n // originDocumentId (its source) and is flagged so operators can tell it apart from a\n // normally-uploaded document; the row's \"view parent / view siblings\" actions drill the relationship.\n const subDocBadge = doc.originDocumentId\n ? ` <span class=\"badge bg-secondary\">${escapeHtmlChars(localization.instant('::Document:SubDocument'))}</span>`\n : '';\n return of(\n `<span class=\"document-file-cell\"><i class=\"${iconClass} me-2\"></i><span class=\"fw-semibold text-truncate\">${escapeHtmlChars(fileName)}</span>${bundleBadge}${subDocBadge}</span>`,\n );\n },\n }),\n EntityProp.create<DocumentListItemDto>({\n type: ePropType.String,\n name: 'documentType',\n displayName: '::Document:Type',\n sortable: false,\n columnWidth: 180,\n valueResolver: data => {\n const typeName = this.documentTypeDisplayName(data.record.documentTypeCode);\n return of(\n typeName\n ? `<span class=\"badge bg-info text-dark\">${escapeHtmlChars(typeName)}</span>`\n : '<span class=\"text-muted\">-</span>',\n );\n },\n }),\n ...fields.map(field => this.createExtractedFieldProp(field)),\n EntityProp.create<DocumentListItemDto>({\n type: ePropType.String,\n name: 'status',\n displayName: '::Document:Status',\n sortable: false,\n columnWidth: 190,\n valueResolver: data => {\n const localization = data.getInjected(LocalizationService);\n const doc = data.record;\n const spinner = this.isProcessingDocument(doc)\n ? '<span class=\"spinner-border spinner-border-sm me-1\" role=\"status\"></span>'\n : '';\n // #284: two badges may stack: lifecycle on the availability axis plus conditional review badge\n // on the review axis. They do not overwrite each other.\n const lifecycle = `<span class=\"${this.getStatusBadgeClass(doc.lifecycleStatus)}\">${spinner}${escapeHtmlChars(localization.instant(this.getStatusLabel(doc.lifecycleStatus)))}</span>`;\n const review = doc.requiresReview\n ? ` <span class=\"badge bg-warning text-dark\">${escapeHtmlChars(localization.instant(this.reviewBadgeLabel(doc)))}</span>`\n : '';\n return of(lifecycle + review);\n },\n }),\n EntityProp.create<DocumentListItemDto>({\n type: ePropType.String,\n name: 'creationTime',\n displayName: '::Document:UploadedAt',\n sortable: true,\n columnWidth: 180,\n valueResolver: data =>\n of(`<span class=\"text-muted small\">${escapeHtmlChars(this.formatCreationTime(data.record.creationTime))}</span>`),\n }),\n ];\n }\n\n private createExtractedFieldProp(field: FieldDefinitionDto): EntityProp<DocumentListItemDto> {\n const fieldName = field.name ?? '';\n const propName = `extracted_${field.id || fieldName}`.replace(/[^A-Za-z0-9_]/g, '_');\n return EntityProp.create<DocumentListItemDto>({\n type: ePropType.String,\n name: propName,\n displayName: field.displayName || field.name || '',\n sortable: false,\n columnWidth: 220,\n valueResolver: data => {\n const text = formatExtractedFieldValue(data.record.extractedFields?.[fieldName]);\n const value = escapeHtmlChars(text);\n return of(`<span class=\"document-field-cell\" title=\"${value}\">${value}</span>`);\n },\n });\n }\n\n private formatCreationTime(value: string | undefined): string {\n if (!value) return '-';\n\n try {\n return formatDate(value, 'yyyy-MM-dd HH:mm', this.locale);\n } catch {\n return value;\n }\n }\n\n // Visible document types for the current layer (Host admin → Host types;\n // tenant admin → that tenant's types). Drives the confirm-classification picker.\n private loadDocumentTypes(): void {\n this.documentTypeService.getVisible()\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: types => {\n this.documentTypes.set(types);\n if (this.typeFilter()) {\n this.loadExtractedFieldColumns(this.typeFilter());\n return;\n }\n this.rebuildTableProps([]);\n },\n error: () => {\n this.documentTypes.set([]);\n this.updateExtractedFieldColumns([]);\n },\n });\n }\n\n // Visible cabinets for the current layer — drives the cabinet filter and the\n // cabinet-name label column (list DTO carries only cabinetId; we map id → name).\n private loadCabinets(): void {\n this.cabinetService.getList()\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: list => this.cabinets.set(list),\n error: () => this.cabinets.set([]),\n });\n }\n\n // The list carries only documentTypeCode as the output contract. Map it to the displayName of a type\n // visible in the current layer; fall back to code when cross-layer or deleted types cannot be resolved.\n // cabinets() remains available for the top filter dropdown.\n documentTypeDisplayName(code: string | null | undefined): string | null {\n if (!code) return null;\n return this.documentTypes().find(t => t.typeCode === code)?.displayName ?? code;\n }\n\n // Load the selected type's field definitions and turn them into dynamic columns\n // (ordered by displayOrder). Cleared when no single type is selected. Errors fall\n // back to no columns rather than breaking the list (mirrors loadDocumentTypes).\n // The type filter is keyed by typeCode (Document exit contract); the field-definition\n // API is keyed by immutable DocumentTypeId (#207), so we resolve code → id via the\n // already-loaded visible types before querying.\n private loadExtractedFieldColumns(typeCode: string): void {\n const documentTypeId = this.documentTypes().find(t => t.typeCode === typeCode)?.id;\n if (!documentTypeId) {\n if (this.typeFilter() === typeCode) {\n this.updateExtractedFieldColumns([]);\n }\n return;\n }\n this.fieldDefinitionService.getList({ documentTypeId })\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: fields => {\n if (this.typeFilter() !== typeCode) return;\n this.updateExtractedFieldColumns(\n [...fields].sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0)),\n );\n },\n error: () => {\n if (this.typeFilter() !== typeCode) return;\n this.updateExtractedFieldColumns([]);\n },\n });\n }\n\n private updateExtractedFieldColumns(fields: FieldDefinitionDto[]): void {\n this.extractedFieldColumns.set(fields);\n this.rebuildTableProps(fields);\n }\n\n onTableActivate(event: TableActivateEvent): void {\n if (event.type !== 'click' || !event.row) return;\n this.openDetail(event.row);\n }\n\n openDetail(doc: DocumentListItemDto): void {\n this.router.navigate(['/documents', doc.id]);\n }\n\n uploadNew(): void {\n this.router.navigate(['/documents']);\n }\n\n delete(doc: DocumentListItemDto, event: Event): void {\n event.stopPropagation();\n this.confirmation\n .warn('::Document:AreYouSureToDelete', '::AreYouSure')\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(status => {\n if (status === Confirmation.Status.confirm) {\n this.documentService.delete(doc.id!)\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: () => {\n this.toaster.success('::Document:DeletedSuccessfully', '::Success');\n this.list.getWithoutPageReset();\n // A deleted document leaves the (soft-delete-filtered) review queue — refresh the badge.\n this.loadReviewQueueCount();\n },\n error: () => this.toaster.error('::Document:DeleteFailed', '::Error'),\n });\n }\n });\n }\n\n // Canonical needs-review total for the toolbar badge. Gated on canConfirm so non-reviewers (who don't\n // see the gateway button) never fire the call. The statistics endpoint shares the review queue's\n // RequiresAttention predicate (#333), so the badge and the queue never drift.\n private loadReviewQueueCount(): void {\n if (!this.canConfirm) return;\n this.statisticsService.get()\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: stats => this.reviewQueueCount.set(stats.needsReviewCount ?? 0),\n error: () => this.reviewQueueCount.set(0),\n });\n }\n\n // #284: show the confirm-classification button only when the document still requires attention\n // (requiresReview, with disposition already considered server-side so rejected documents no longer\n // require attention) and classification is unresolved. Missing required fields are completed on the\n // detail page.\n needsConfirmation(doc: DocumentListItemDto): boolean {\n return doc.requiresReview === true &&\n ((doc.reviewReasons ?? DocumentReviewReasons.None) & DocumentReviewReasons.UnresolvedClassification)\n !== DocumentReviewReasons.None;\n }\n\n // #284: pure availability axis. Removed the old review-mixed judgment; after the two axes became\n // orthogonal, the two badges render independently and are no longer mutually exclusive.\n isProcessingDocument(doc: DocumentListItemDto): boolean {\n return doc.lifecycleStatus === DocumentLifecycleStatus.Processing ||\n doc.lifecycleStatus === DocumentLifecycleStatus.Uploaded;\n }\n\n openConfirmDialog(doc: DocumentListItemDto, event: Event): void {\n event.stopPropagation();\n this.confirmingDoc.set(doc);\n // Pre-select the document's current (low-confidence) classification when present,\n // so the operator usually just confirms; otherwise force an explicit choice. The\n // confirm command is keyed by immutable DocumentTypeId (#207), so resolve the\n // document's exit-contract typeCode → id via the already-loaded visible types.\n this.selectedTypeId.set(\n this.documentTypes().find(t => t.typeCode === doc.documentTypeCode)?.id ?? '',\n );\n }\n\n closeConfirmDialog(): void {\n this.confirmingDoc.set(null);\n this.selectedTypeId.set('');\n }\n\n submitConfirmation(): void {\n const doc = this.confirmingDoc();\n if (!doc || !this.selectedTypeId()) return;\n this.isConfirming.set(true);\n this.documentService.confirmClassification(doc.id!, { documentTypeId: this.selectedTypeId() })\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: () => {\n this.isConfirming.set(false);\n this.closeConfirmDialog();\n this.toaster.success('::Document:ClassificationConfirmed', '::Success');\n this.list.getWithoutPageReset();\n // Confirming a classification clears its review reason — keep the badge in step with the queue.\n this.loadReviewQueueCount();\n },\n error: () => {\n this.isConfirming.set(false);\n this.toaster.error('::Document:ConfirmFailed', '::Error');\n },\n });\n }\n\n getStatusBadgeClass(status: DocumentLifecycleStatus | undefined): string {\n switch (status) {\n case DocumentLifecycleStatus.Uploaded:\n return 'badge bg-secondary';\n case DocumentLifecycleStatus.Processing:\n return 'badge bg-warning text-dark';\n case DocumentLifecycleStatus.Ready:\n return 'badge bg-success';\n case DocumentLifecycleStatus.Failed:\n return 'badge bg-danger';\n default:\n return 'badge bg-secondary';\n }\n }\n\n // #284: review badge text follows the reason: classification confirmation pending or required fields\n // missing. The client renders only reviewReasons provided by the server.\n reviewBadgeLabel(doc: DocumentListItemDto): string {\n const reasons = doc.reviewReasons ?? DocumentReviewReasons.None;\n if ((reasons & DocumentReviewReasons.UnresolvedClassification) !== DocumentReviewReasons.None) {\n return '::Document:ReviewReason:UnresolvedClassification';\n }\n if ((reasons & DocumentReviewReasons.MissingRequiredFields) !== DocumentReviewReasons.None) {\n return '::Document:ReviewReason:MissingRequiredFields';\n }\n if ((reasons & DocumentReviewReasons.SegmentationIncomplete) !== DocumentReviewReasons.None) {\n return '::Document:ReviewReason:SegmentationIncomplete';\n }\n return '::Document:NeedsReview';\n }\n\n getStatusLabel(status: DocumentLifecycleStatus | undefined): string {\n switch (status) {\n case DocumentLifecycleStatus.Uploaded:\n return '::Document:Status:Uploaded';\n case DocumentLifecycleStatus.Processing:\n return '::Document:Status:Processing';\n case DocumentLifecycleStatus.Ready:\n return '::Document:Status:Ready';\n case DocumentLifecycleStatus.Failed:\n return '::Document:Status:Failed';\n default:\n return '::Document:Status:Unknown';\n }\n }\n\n isImage(doc: DocumentListItemDto): boolean {\n return doc.fileOrigin?.contentType?.startsWith('image/') ?? false;\n }\n}\n","<div class=\"container-fluid py-4\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <h4 class=\"mb-0\">\n <i class=\"fas fa-file-alt me-2\"></i>\n {{ '::Document:Documents' | abpLocalization }}\n </h4>\n <div class=\"d-flex gap-2\">\n <button\n class=\"btn btn-outline-secondary\"\n (click)=\"refresh()\"\n [disabled]=\"isLoading()\"\n title=\"{{ '::Refresh' | abpLocalization }}\"\n >\n <i class=\"fas fa-sync-alt\" [class.fa-spin]=\"isLoading()\"></i>\n </button>\n @if (canConfirm) {\n <button\n class=\"btn\"\n [class.btn-warning]=\"reviewFilter()\"\n [class.btn-outline-warning]=\"!reviewFilter()\"\n [attr.aria-pressed]=\"reviewFilter()\"\n (click)=\"toggleReviewFilter()\"\n title=\"{{ '::Document:NeedsReview' | abpLocalization }}\"\n >\n <i class=\"fas fa-clipboard-check me-1\"></i>\n {{ '::Document:NeedsReview' | abpLocalization }}\n @if (reviewQueueCount() > 0) {\n <span class=\"badge bg-danger ms-1\">{{ reviewQueueCount() }}</span>\n }\n </button>\n }\n @if (canUpload) {\n <button class=\"btn btn-primary\" (click)=\"uploadNew()\">\n <i class=\"fas fa-upload me-1\"></i>\n {{ '::Document:Upload' | abpLocalization }}\n </button>\n }\n </div>\n </div>\n\n <!-- Filters -->\n <div class=\"d-flex flex-wrap gap-2 mb-3\">\n <select\n class=\"form-select form-select-sm w-auto\"\n [ngModel]=\"lifecycleFilter()\"\n (ngModelChange)=\"onLifecycleFilterChange($event)\"\n >\n <option [ngValue]=\"undefined\">{{ '::Document:Filter:AllStatuses' | abpLocalization }}</option>\n <option [ngValue]=\"DocumentLifecycleStatus.Uploaded\">{{ '::Document:Status:Uploaded' | abpLocalization }}</option>\n <option [ngValue]=\"DocumentLifecycleStatus.Processing\">{{ '::Document:Status:Processing' | abpLocalization }}</option>\n <option [ngValue]=\"DocumentLifecycleStatus.Ready\">{{ '::Document:Status:Ready' | abpLocalization }}</option>\n <option [ngValue]=\"DocumentLifecycleStatus.Failed\">{{ '::Document:Status:Failed' | abpLocalization }}</option>\n </select>\n @if (documentTypes().length > 0) {\n <select\n class=\"form-select form-select-sm w-auto\"\n [ngModel]=\"typeFilter()\"\n (ngModelChange)=\"onTypeFilterChange($event)\"\n >\n <option value=\"\">{{ '::Document:Filter:AllTypes' | abpLocalization }}</option>\n @for (t of documentTypes(); track t.id) {\n <option [value]=\"t.typeCode\">{{ t.displayName }}</option>\n }\n </select>\n }\n @if (canViewCabinets && cabinets().length > 0) {\n <select\n class=\"form-select form-select-sm w-auto\"\n [ngModel]=\"cabinetFilter()\"\n (ngModelChange)=\"onCabinetFilterChange($event)\"\n >\n <option value=\"\">{{ '::Document:Filter:AllCabinets' | abpLocalization }}</option>\n @for (c of cabinets(); track c.id) {\n <option [value]=\"c.id\">{{ c.name }}</option>\n }\n </select>\n }\n </div>\n\n <!-- Sub-documents filter indicator (#354) -->\n @if (originDocumentIdFilter()) {\n <div class=\"alert alert-info d-flex justify-content-between align-items-center py-2 mb-3\">\n <span>\n <i class=\"fas fa-sitemap me-2\"></i>\n {{ '::Document:ViewingSubDocumentsOf' | abpLocalization }}\n <strong>{{\n subDocumentsParent()?.title ||\n subDocumentsParent()?.fileOrigin?.originalFileName ||\n originDocumentIdFilter()\n }}</strong>\n </span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"clearSubDocumentsFilter()\">\n <i class=\"fas fa-times me-1\"></i>\n {{ '::Document:ShowAllDocuments' | abpLocalization }}\n </button>\n </div>\n }\n\n <!-- Loading spinner -->\n @if (isLoading() && documents().items.length === 0) {\n <div class=\"text-center py-5\">\n <div class=\"spinner-border text-primary\" role=\"status\"></div>\n </div>\n }\n\n <!-- Empty state: message only — the \"upload your first document\" CTA was removed; upload lives in the\n header toolbar. -->\n @if (!isLoading() && documents().totalCount === 0) {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center py-5\">\n <i class=\"fas fa-inbox fa-3x text-muted mb-3 d-block\"></i>\n <p class=\"text-muted mb-0\">{{ '::Document:NoDocuments' | abpLocalization }}</p>\n </div>\n </div>\n }\n\n <!-- Document list -->\n @if (documents().totalCount > 0) {\n <div class=\"card shadow-sm document-list-table\">\n <div class=\"card-body p-0\">\n <ng-template #actionsTemplate let-doc>\n @if ((needsConfirmation(doc) && canConfirm) || canDelete || doc.isContainer || doc.originDocumentId) {\n <div ngbDropdown container=\"body\" class=\"d-inline-block\" (click)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"btn btn-sm btn-primary\" ngbDropdownToggle>\n {{ 'AbpUi::Actions' | abpLocalization }}\n </button>\n <div ngbDropdownMenu>\n @if (doc.isContainer) {\n <button type=\"button\" ngbDropdownItem (click)=\"viewSubDocuments(doc, $event)\">\n <i class=\"fas fa-sitemap me-2\"></i>\n {{ '::Document:ViewSubDocuments' | abpLocalization }}\n </button>\n }\n @if (doc.originDocumentId) {\n <button type=\"button\" ngbDropdownItem (click)=\"openParentDocument(doc, $event)\">\n <i class=\"fas fa-arrow-up me-2\"></i>\n {{ '::Document:ViewParentDocument' | abpLocalization }}\n </button>\n <button type=\"button\" ngbDropdownItem (click)=\"viewSiblingDocuments(doc, $event)\">\n <i class=\"fas fa-sitemap me-2\"></i>\n {{ '::Document:ViewSiblingDocuments' | abpLocalization }}\n </button>\n }\n @if (needsConfirmation(doc) && canConfirm) {\n <button type=\"button\" ngbDropdownItem (click)=\"openConfirmDialog(doc, $event)\">\n <i class=\"fas fa-check me-2\"></i>\n {{ '::Document:ConfirmClassification' | abpLocalization }}\n </button>\n }\n @if (canDelete) {\n <button type=\"button\" ngbDropdownItem (click)=\"delete(doc, $event)\">\n <i class=\"fas fa-trash me-2\"></i>\n {{ '::Delete' | abpLocalization }}\n </button>\n }\n </div>\n </div>\n }\n </ng-template>\n\n @for (key of [tableKey()]; track key) {\n @if (showActionsColumn()) {\n <abp-extensible-table\n [data]=\"documents().items\"\n [recordsTotal]=\"documents().totalCount\"\n [list]=\"list\"\n [actionsTemplate]=\"actionsTemplate\"\n actionsText=\"AbpUi::Actions\"\n [actionsColumnWidth]=\"150\"\n (tableActivate)=\"onTableActivate($event)\"\n />\n } @else {\n <abp-extensible-table\n [data]=\"documents().items\"\n [recordsTotal]=\"documents().totalCount\"\n [list]=\"list\"\n (tableActivate)=\"onTableActivate($event)\"\n />\n }\n }\n </div>\n </div>\n }\n</div>\n\n<!-- Manual Classification Confirm Modal -->\n@if (confirmingDoc(); as doc) {\n <div class=\"modal d-block\" tabindex=\"-1\" style=\"background:rgba(0,0,0,.4);\" (click)=\"closeConfirmDialog()\">\n <div class=\"modal-dialog modal-dialog-centered\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"fas fa-user-edit me-2\"></i>\n {{ '::Document:ConfirmClassification' | abpLocalization }}\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeConfirmDialog()\"></button>\n </div>\n <div class=\"modal-body\">\n <p class=\"text-muted small mb-3\">\n {{ doc.title || doc.fileOrigin?.originalFileName }}\n </p>\n <label class=\"form-label fw-semibold\">{{ '::Document:SelectDocumentType' | abpLocalization }}</label>\n <select\n class=\"form-select\"\n [ngModel]=\"selectedTypeId()\"\n (ngModelChange)=\"selectedTypeId.set($event)\"\n >\n <option value=\"\" disabled>{{ '::Document:SelectDocumentType' | abpLocalization }}</option>\n @for (t of documentTypes(); track t.id) {\n <option [value]=\"t.id\">{{ t.displayName }} ({{ t.typeCode }})</option>\n }\n </select>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"closeConfirmDialog()\">\n {{ '::Cancel' | abpLocalization }}\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n [disabled]=\"!selectedTypeId() || isConfirming()\"\n (click)=\"submitConfirmation()\"\n >\n @if (isConfirming()) {\n <span class=\"spinner-border spinner-border-sm me-1\"></span>\n }\n {{ '::Document:Confirm' | abpLocalization }}\n </button>\n </div>\n </div>\n </div>\n </div>\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, DestroyRef, Injector, viewChild, signal, computed, afterNextRender, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
4
|
+
import { CommonModule } from '@angular/common';
|
|
5
|
+
import * as i2 from '@angular/router';
|
|
6
|
+
import { RouterModule } from '@angular/router';
|
|
7
|
+
import { PermissionService, LocalizationPipe } from '@abp/ng.core';
|
|
8
|
+
import { DocumentUploadService, CabinetService, EXTRACT_PERMISSIONS, DocumentStatisticsService, DocumentTypeService } from '@dignite/vault-extract';
|
|
9
|
+
import { from, of, Subject, EMPTY } from 'rxjs';
|
|
10
|
+
import { mergeMap, map, catchError, tap, switchMap } from 'rxjs/operators';
|
|
11
|
+
import * as i1 from '@angular/forms';
|
|
12
|
+
import { FormsModule } from '@angular/forms';
|
|
13
|
+
import { ToasterService } from '@abp/ng.theme.shared';
|
|
14
|
+
import { f as formatBytes } from './dignite-vault-extract-documents-format-bytes-Cd3QwfQZ.mjs';
|
|
15
|
+
|
|
16
|
+
// Client-side upload constraints — MUST mirror the backend DocumentConsts
|
|
17
|
+
// (core/src/Dignite.Vault.Extract.Domain.Shared/Documents/DocumentConsts.cs:
|
|
18
|
+
// MaxUploadFileBytes / AllowedUploadContentTypes / AllowedUploadExtensions, #221).
|
|
19
|
+
// These are a UX nicety (instant feedback + file-picker filtering); the backend
|
|
20
|
+
// is the authoritative fail-closed gate. If the backend defaults change, change
|
|
21
|
+
// these to match — single source of truth on the Angular side lives here.
|
|
22
|
+
/** Maximum upload size in bytes (20 MiB). Mirrors DocumentConsts.MaxUploadFileBytes. */
|
|
23
|
+
const MAX_UPLOAD_FILE_BYTES = 20 * 1024 * 1024;
|
|
24
|
+
/** Allowed MIME types. Mirrors DocumentConsts.AllowedUploadContentTypes. */
|
|
25
|
+
const ALLOWED_UPLOAD_CONTENT_TYPES = [
|
|
26
|
+
'image/jpeg',
|
|
27
|
+
'image/png',
|
|
28
|
+
'image/gif',
|
|
29
|
+
'image/webp',
|
|
30
|
+
'application/pdf',
|
|
31
|
+
];
|
|
32
|
+
/** Allowed file extensions (lowercase, leading dot). Mirrors DocumentConsts.AllowedUploadExtensions. */
|
|
33
|
+
const ALLOWED_UPLOAD_EXTENSIONS = [
|
|
34
|
+
'.jpg',
|
|
35
|
+
'.jpeg',
|
|
36
|
+
'.png',
|
|
37
|
+
'.gif',
|
|
38
|
+
'.webp',
|
|
39
|
+
'.pdf',
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Value for a file input's `accept` attribute, derived from the whitelists so the
|
|
43
|
+
* native picker filters to exactly the allowed set (no `image/*` over-reach).
|
|
44
|
+
*/
|
|
45
|
+
const UPLOAD_ACCEPT_ATTRIBUTE = [
|
|
46
|
+
...ALLOWED_UPLOAD_CONTENT_TYPES,
|
|
47
|
+
...ALLOWED_UPLOAD_EXTENSIONS,
|
|
48
|
+
].join(',');
|
|
49
|
+
/** True when both the MIME type and the extension are in their respective whitelists (mirrors the backend dual check). */
|
|
50
|
+
function isAllowedUpload(file) {
|
|
51
|
+
const typeOk = ALLOWED_UPLOAD_CONTENT_TYPES.includes(file.type);
|
|
52
|
+
const dot = file.name.lastIndexOf('.');
|
|
53
|
+
const ext = dot >= 0 ? file.name.slice(dot).toLowerCase() : '';
|
|
54
|
+
const extOk = ALLOWED_UPLOAD_EXTENSIONS.includes(ext);
|
|
55
|
+
return typeOk && extOk;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Limits the number of concurrent /api/documents/upload requests to avoid
|
|
59
|
+
// exhausting the browser's per-origin connection pool and overloading the
|
|
60
|
+
// server when the user drops dozens of files at once.
|
|
61
|
+
const MAX_CONCURRENT_UPLOADS = 3;
|
|
62
|
+
class DocumentUploadComponent {
|
|
63
|
+
constructor() {
|
|
64
|
+
this.documentUploadService = inject(DocumentUploadService);
|
|
65
|
+
this.cabinetService = inject(CabinetService);
|
|
66
|
+
this.toaster = inject(ToasterService);
|
|
67
|
+
this.permissionService = inject(PermissionService);
|
|
68
|
+
this.destroyRef = inject(DestroyRef);
|
|
69
|
+
this.injector = inject(Injector);
|
|
70
|
+
// Primary file-picker trigger; used to restore focus after the result queue is
|
|
71
|
+
// cleared, because the button the user just clicked is removed from the DOM.
|
|
72
|
+
this.browseButton = viewChild('browseButton', ...(ngDevMode ? [{ debugName: "browseButton" }] : /* istanbul ignore next */ []));
|
|
73
|
+
// Cabinet selection requires Cabinets.Default permission because backend getList is [Authorize].
|
|
74
|
+
// Without permission, hide the dropdown and upload as unclassified.
|
|
75
|
+
this.canViewCabinets = this.permissionService.getGrantedPolicy(EXTRACT_PERMISSIONS.Cabinets.Default);
|
|
76
|
+
this.cabinets = signal([], ...(ngDevMode ? [{ debugName: "cabinets" }] : /* istanbul ignore next */ []));
|
|
77
|
+
this.selectedCabinetId = signal('', ...(ngDevMode ? [{ debugName: "selectedCabinetId" }] : /* istanbul ignore next */ []));
|
|
78
|
+
// Picker `accept` filter, derived from the shared whitelist (mirrors backend, #221).
|
|
79
|
+
this.acceptAttribute = UPLOAD_ACCEPT_ATTRIBUTE;
|
|
80
|
+
this.isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : /* istanbul ignore next */ []));
|
|
81
|
+
this.isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : /* istanbul ignore next */ []));
|
|
82
|
+
this.uploadingFiles = signal([], ...(ngDevMode ? [{ debugName: "uploadingFiles" }] : /* istanbul ignore next */ []));
|
|
83
|
+
this.hasUploadResults = computed(() => this.uploadingFiles().length > 0 &&
|
|
84
|
+
!this.isUploading() &&
|
|
85
|
+
this.uploadingFiles().every(file => file.done || file.error), ...(ngDevMode ? [{ debugName: "hasUploadResults" }] : /* istanbul ignore next */ []));
|
|
86
|
+
this.hasUploadErrors = computed(() => this.hasUploadResults() && this.uploadingFiles().some(file => file.error), ...(ngDevMode ? [{ debugName: "hasUploadErrors" }] : /* istanbul ignore next */ []));
|
|
87
|
+
this.successfulUploadCount = computed(() => this.uploadingFiles().filter(file => file.done).length, ...(ngDevMode ? [{ debugName: "successfulUploadCount" }] : /* istanbul ignore next */ []));
|
|
88
|
+
this.canAcceptFiles = computed(() => !this.isUploading() && !this.hasUploadResults(), ...(ngDevMode ? [{ debugName: "canAcceptFiles" }] : /* istanbul ignore next */ []));
|
|
89
|
+
}
|
|
90
|
+
ngOnInit() {
|
|
91
|
+
if (this.canViewCabinets) {
|
|
92
|
+
this.cabinetService.getList()
|
|
93
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
94
|
+
.subscribe({
|
|
95
|
+
next: list => this.cabinets.set(list),
|
|
96
|
+
error: () => this.cabinets.set([]),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
onDragOver(event) {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
event.stopPropagation();
|
|
103
|
+
if (!this.canAcceptFiles())
|
|
104
|
+
return;
|
|
105
|
+
this.isDragOver.set(true);
|
|
106
|
+
}
|
|
107
|
+
onDragLeave(event) {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
event.stopPropagation();
|
|
110
|
+
this.isDragOver.set(false);
|
|
111
|
+
}
|
|
112
|
+
onDrop(event) {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
event.stopPropagation();
|
|
115
|
+
this.isDragOver.set(false);
|
|
116
|
+
if (!this.canAcceptFiles())
|
|
117
|
+
return;
|
|
118
|
+
const files = event.dataTransfer?.files;
|
|
119
|
+
if (files && files.length > 0) {
|
|
120
|
+
this.uploadFiles(Array.from(files));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
onFileSelected(event) {
|
|
124
|
+
const input = event.target;
|
|
125
|
+
if (input.files && input.files.length > 0) {
|
|
126
|
+
this.uploadFiles(Array.from(input.files));
|
|
127
|
+
input.value = '';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
openFilePicker(input) {
|
|
131
|
+
if (!this.canAcceptFiles())
|
|
132
|
+
return;
|
|
133
|
+
input.click();
|
|
134
|
+
}
|
|
135
|
+
resetQueue() {
|
|
136
|
+
this.uploadingFiles.set([]);
|
|
137
|
+
this.isDragOver.set(false);
|
|
138
|
+
// Restore focus to the primary trigger once the empty state re-renders;
|
|
139
|
+
// the button the user just clicked is removed when the queue is cleared.
|
|
140
|
+
afterNextRender(() => this.browseButton()?.nativeElement.focus(), {
|
|
141
|
+
injector: this.injector,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
uploadFiles(files) {
|
|
145
|
+
if (!this.canAcceptFiles())
|
|
146
|
+
return;
|
|
147
|
+
// Mirror the backend fail-closed gate (#221): MIME + extension whitelist, then size.
|
|
148
|
+
const valid = files.filter(f => {
|
|
149
|
+
if (!isAllowedUpload(f)) {
|
|
150
|
+
this.toaster.error('::Document:UnsupportedFileType', '::Error');
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (f.size > MAX_UPLOAD_FILE_BYTES) {
|
|
154
|
+
this.toaster.error('::Document:FileTooLarge', '::Error');
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
});
|
|
159
|
+
if (valid.length === 0)
|
|
160
|
+
return;
|
|
161
|
+
this.isUploading.set(true);
|
|
162
|
+
this.uploadingFiles.set(valid.map((file, index) => ({
|
|
163
|
+
key: `${file.name}-${file.lastModified}-${file.size}-${index}`,
|
|
164
|
+
name: file.name,
|
|
165
|
+
done: false,
|
|
166
|
+
error: false,
|
|
167
|
+
})));
|
|
168
|
+
const indexed = valid.map((file, idx) => ({ file, idx }));
|
|
169
|
+
from(indexed)
|
|
170
|
+
.pipe(mergeMap(({ file, idx }) => this.documentUploadService.upload(file, this.selectedCabinetId() || undefined).pipe(map(document => ({
|
|
171
|
+
idx,
|
|
172
|
+
success: true,
|
|
173
|
+
documentId: document.id,
|
|
174
|
+
errorMessage: undefined,
|
|
175
|
+
})), catchError(err => {
|
|
176
|
+
const errorMessage = err?.error?.error?.message;
|
|
177
|
+
return of({ idx, success: false, documentId: undefined, errorMessage });
|
|
178
|
+
})), MAX_CONCURRENT_UPLOADS), takeUntilDestroyed(this.destroyRef))
|
|
179
|
+
.subscribe({
|
|
180
|
+
next: ({ idx, success, documentId, errorMessage }) => {
|
|
181
|
+
this.uploadingFiles.update(list => list.map((item, j) => j === idx
|
|
182
|
+
? {
|
|
183
|
+
...item,
|
|
184
|
+
done: success,
|
|
185
|
+
error: !success,
|
|
186
|
+
documentId: success ? documentId : undefined,
|
|
187
|
+
errorMessage,
|
|
188
|
+
}
|
|
189
|
+
: item));
|
|
190
|
+
},
|
|
191
|
+
complete: () => {
|
|
192
|
+
this.isUploading.set(false);
|
|
193
|
+
const hasError = this.uploadingFiles().some(f => f.error);
|
|
194
|
+
if (!hasError) {
|
|
195
|
+
this.toaster.success('::Document:UploadedSuccessfully', '::Success');
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DocumentUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
201
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: DocumentUploadComponent, isStandalone: true, selector: "lib-document-upload", viewQueries: [{ propertyName: "browseButton", first: true, predicate: ["browseButton"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"card document-upload-card shadow-sm\">\n <div class=\"card-body p-4 p-lg-5\">\n <div class=\"d-flex flex-wrap align-items-start justify-content-between gap-3 mb-4\">\n <div>\n <h2 class=\"h4 mb-2\">{{ '::Document:Home:UploadTitle' | abpLocalization }}</h2>\n <p class=\"text-muted mb-0\">{{ '::Document:Home:UploadSubtitle' | abpLocalization }}</p>\n </div>\n\n @if (canViewCabinets && cabinets().length > 0 && canAcceptFiles()) {\n <div class=\"cabinet-select\">\n <label class=\"form-label fw-semibold\">{{ '::Document:SelectCabinet' | abpLocalization }}</label>\n <select\n class=\"form-select\"\n [ngModel]=\"selectedCabinetId()\"\n (ngModelChange)=\"selectedCabinetId.set($event)\"\n >\n <option value=\"\">{{ '::Document:Unfiled' | abpLocalization }}</option>\n @for (c of cabinets(); track c.id) {\n <option [value]=\"c.id\">{{ c.name }}</option>\n }\n </select>\n </div>\n }\n </div>\n\n <input\n #filePicker\n type=\"file\"\n [attr.accept]=\"acceptAttribute\"\n multiple\n class=\"d-none\"\n (change)=\"onFileSelected($event)\"\n />\n <input\n #cameraPicker\n type=\"file\"\n accept=\"image/*\"\n capture=\"environment\"\n class=\"d-none\"\n (change)=\"onFileSelected($event)\"\n />\n\n <div\n class=\"drop-zone\"\n [class.drag-over]=\"isDragOver()\"\n [class.uploading]=\"isUploading()\"\n [class.has-results]=\"hasUploadResults()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n >\n @if (isUploading() || hasUploadResults()) {\n <div class=\"upload-results w-100\">\n <div class=\"d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3\">\n <div>\n <p class=\"fw-semibold mb-1\">\n @if (isUploading()) {\n {{ '::Document:Uploading' | abpLocalization }}\n } @else if (hasUploadErrors()) {\n {{ '::Document:Upload:CompletedWithErrors' | abpLocalization }}\n } @else {\n {{ '::Document:Upload:Complete' | abpLocalization }}\n }\n </p>\n <small class=\"text-muted\">\n {{ successfulUploadCount() }} / {{ uploadingFiles().length }}\n {{ '::Document:Upload:Succeeded' | abpLocalization }}\n </small>\n </div>\n </div>\n\n <ul class=\"list-group upload-queue\">\n @for (f of uploadingFiles(); track f.key) {\n <li class=\"list-group-item py-3\">\n <div class=\"d-flex align-items-start gap-3\">\n @if (f.done) {\n <span class=\"status-icon text-bg-success\"><i class=\"fas fa-check\"></i></span>\n } @else if (f.error) {\n <span class=\"status-icon text-bg-danger\"><i class=\"fas fa-times\"></i></span>\n } @else {\n <span class=\"spinner-border spinner-border-sm text-primary mt-1\" role=\"status\"></span>\n }\n\n <div class=\"min-width-0 flex-grow-1\">\n <div class=\"fw-semibold text-truncate\">{{ f.name }}</div>\n @if (f.errorMessage) {\n <small class=\"text-danger d-block mt-1\">{{ f.errorMessage }}</small>\n } @else if (f.error) {\n <small class=\"text-danger\">{{ '::Document:UploadFailed' | abpLocalization }}</small>\n } @else if (f.done) {\n <small class=\"text-muted\">{{ '::Document:UploadedSuccessfully' | abpLocalization }}</small>\n } @else {\n <small class=\"text-muted\">{{ '::Document:Uploading' | abpLocalization }}</small>\n }\n </div>\n\n @if (hasUploadResults() && f.done && f.documentId) {\n <a\n class=\"btn btn-sm btn-outline-primary flex-shrink-0\"\n [routerLink]=\"['/documents', f.documentId]\"\n >\n <i class=\"fas fa-external-link-alt me-1\"></i>\n {{ '::Document:OpenDocument' | abpLocalization }}\n </a>\n }\n </div>\n </li>\n }\n </ul>\n\n @if (hasUploadResults()) {\n <div class=\"d-flex flex-wrap justify-content-end gap-2 mt-3\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"resetQueue()\">\n <i class=\"fas fa-plus me-1\"></i>\n {{ '::Document:ContinueUploading' | abpLocalization }}\n </button>\n <a class=\"btn btn-primary\" routerLink=\"/documents/list\">\n <i class=\"fas fa-list me-1\"></i>\n {{ '::Document:ContinueToDocuments' | abpLocalization }}\n </a>\n </div>\n }\n </div>\n } @else {\n <div class=\"text-center py-5\">\n <span class=\"upload-icon mb-3\">\n <i class=\"fas fa-cloud-upload-alt\"></i>\n </span>\n <h3 class=\"h5 mb-2\">{{ '::Document:DropOrClick' | abpLocalization }}</h3>\n <p class=\"text-muted mb-4\">{{ '::Document:SupportedFormats' | abpLocalization }}</p>\n\n <div class=\"d-flex flex-wrap justify-content-center gap-2\">\n <button #browseButton type=\"button\" class=\"btn btn-primary\" (click)=\"openFilePicker(filePicker)\">\n <i class=\"fas fa-folder-open me-1\"></i>\n {{ '::Document:BrowseFiles' | abpLocalization }}\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"openFilePicker(cameraPicker)\">\n <i class=\"fas fa-camera me-1\"></i>\n {{ '::Document:TakePhoto' | abpLocalization }}\n </button>\n </div>\n </div>\n }\n </div>\n\n <div class=\"mt-3 text-center\">\n <small class=\"text-muted\">\n <i class=\"fas fa-info-circle me-1\"></i>\n {{ '::Document:MaxFileSize' | abpLocalization }}\n </small>\n </div>\n </div>\n</div>\n", styles: [".document-upload-card{border:1px solid var(--bs-border-color)}.cabinet-select{min-width:min(100%,18rem)}.drop-zone{border:2px dashed var(--bs-border-color, #dee2e6);border-radius:.5rem;background-color:var(--bs-tertiary-bg, #f8f9fa);transition:border-color .2s ease,background-color .2s ease,box-shadow .2s ease;min-height:24rem;display:flex;align-items:center;justify-content:center;padding:1.5rem}.drop-zone.drag-over{border-color:var(--bs-primary, #0d6efd);background-color:#0d6efd0d;box-shadow:0 0 0 .25rem #0d6efd14}.drop-zone.uploading,.drop-zone.has-results{cursor:not-allowed}.upload-icon{width:4rem;height:4rem;border-radius:.5rem;display:inline-flex;align-items:center;justify-content:center;background-color:#0d6efd14;color:var(--bs-primary, #0d6efd);font-size:1.8rem}.upload-queue{max-height:24rem;overflow:auto}.status-icon{width:1.5rem;height:1.5rem;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;font-size:.75rem}.min-width-0{min-width:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: LocalizationPipe, name: "abpLocalization" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
202
|
+
}
|
|
203
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DocumentUploadComponent, decorators: [{
|
|
204
|
+
type: Component,
|
|
205
|
+
args: [{ selector: 'lib-document-upload', imports: [CommonModule, FormsModule, LocalizationPipe, RouterModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"card document-upload-card shadow-sm\">\n <div class=\"card-body p-4 p-lg-5\">\n <div class=\"d-flex flex-wrap align-items-start justify-content-between gap-3 mb-4\">\n <div>\n <h2 class=\"h4 mb-2\">{{ '::Document:Home:UploadTitle' | abpLocalization }}</h2>\n <p class=\"text-muted mb-0\">{{ '::Document:Home:UploadSubtitle' | abpLocalization }}</p>\n </div>\n\n @if (canViewCabinets && cabinets().length > 0 && canAcceptFiles()) {\n <div class=\"cabinet-select\">\n <label class=\"form-label fw-semibold\">{{ '::Document:SelectCabinet' | abpLocalization }}</label>\n <select\n class=\"form-select\"\n [ngModel]=\"selectedCabinetId()\"\n (ngModelChange)=\"selectedCabinetId.set($event)\"\n >\n <option value=\"\">{{ '::Document:Unfiled' | abpLocalization }}</option>\n @for (c of cabinets(); track c.id) {\n <option [value]=\"c.id\">{{ c.name }}</option>\n }\n </select>\n </div>\n }\n </div>\n\n <input\n #filePicker\n type=\"file\"\n [attr.accept]=\"acceptAttribute\"\n multiple\n class=\"d-none\"\n (change)=\"onFileSelected($event)\"\n />\n <input\n #cameraPicker\n type=\"file\"\n accept=\"image/*\"\n capture=\"environment\"\n class=\"d-none\"\n (change)=\"onFileSelected($event)\"\n />\n\n <div\n class=\"drop-zone\"\n [class.drag-over]=\"isDragOver()\"\n [class.uploading]=\"isUploading()\"\n [class.has-results]=\"hasUploadResults()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n >\n @if (isUploading() || hasUploadResults()) {\n <div class=\"upload-results w-100\">\n <div class=\"d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3\">\n <div>\n <p class=\"fw-semibold mb-1\">\n @if (isUploading()) {\n {{ '::Document:Uploading' | abpLocalization }}\n } @else if (hasUploadErrors()) {\n {{ '::Document:Upload:CompletedWithErrors' | abpLocalization }}\n } @else {\n {{ '::Document:Upload:Complete' | abpLocalization }}\n }\n </p>\n <small class=\"text-muted\">\n {{ successfulUploadCount() }} / {{ uploadingFiles().length }}\n {{ '::Document:Upload:Succeeded' | abpLocalization }}\n </small>\n </div>\n </div>\n\n <ul class=\"list-group upload-queue\">\n @for (f of uploadingFiles(); track f.key) {\n <li class=\"list-group-item py-3\">\n <div class=\"d-flex align-items-start gap-3\">\n @if (f.done) {\n <span class=\"status-icon text-bg-success\"><i class=\"fas fa-check\"></i></span>\n } @else if (f.error) {\n <span class=\"status-icon text-bg-danger\"><i class=\"fas fa-times\"></i></span>\n } @else {\n <span class=\"spinner-border spinner-border-sm text-primary mt-1\" role=\"status\"></span>\n }\n\n <div class=\"min-width-0 flex-grow-1\">\n <div class=\"fw-semibold text-truncate\">{{ f.name }}</div>\n @if (f.errorMessage) {\n <small class=\"text-danger d-block mt-1\">{{ f.errorMessage }}</small>\n } @else if (f.error) {\n <small class=\"text-danger\">{{ '::Document:UploadFailed' | abpLocalization }}</small>\n } @else if (f.done) {\n <small class=\"text-muted\">{{ '::Document:UploadedSuccessfully' | abpLocalization }}</small>\n } @else {\n <small class=\"text-muted\">{{ '::Document:Uploading' | abpLocalization }}</small>\n }\n </div>\n\n @if (hasUploadResults() && f.done && f.documentId) {\n <a\n class=\"btn btn-sm btn-outline-primary flex-shrink-0\"\n [routerLink]=\"['/documents', f.documentId]\"\n >\n <i class=\"fas fa-external-link-alt me-1\"></i>\n {{ '::Document:OpenDocument' | abpLocalization }}\n </a>\n }\n </div>\n </li>\n }\n </ul>\n\n @if (hasUploadResults()) {\n <div class=\"d-flex flex-wrap justify-content-end gap-2 mt-3\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"resetQueue()\">\n <i class=\"fas fa-plus me-1\"></i>\n {{ '::Document:ContinueUploading' | abpLocalization }}\n </button>\n <a class=\"btn btn-primary\" routerLink=\"/documents/list\">\n <i class=\"fas fa-list me-1\"></i>\n {{ '::Document:ContinueToDocuments' | abpLocalization }}\n </a>\n </div>\n }\n </div>\n } @else {\n <div class=\"text-center py-5\">\n <span class=\"upload-icon mb-3\">\n <i class=\"fas fa-cloud-upload-alt\"></i>\n </span>\n <h3 class=\"h5 mb-2\">{{ '::Document:DropOrClick' | abpLocalization }}</h3>\n <p class=\"text-muted mb-4\">{{ '::Document:SupportedFormats' | abpLocalization }}</p>\n\n <div class=\"d-flex flex-wrap justify-content-center gap-2\">\n <button #browseButton type=\"button\" class=\"btn btn-primary\" (click)=\"openFilePicker(filePicker)\">\n <i class=\"fas fa-folder-open me-1\"></i>\n {{ '::Document:BrowseFiles' | abpLocalization }}\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"openFilePicker(cameraPicker)\">\n <i class=\"fas fa-camera me-1\"></i>\n {{ '::Document:TakePhoto' | abpLocalization }}\n </button>\n </div>\n </div>\n }\n </div>\n\n <div class=\"mt-3 text-center\">\n <small class=\"text-muted\">\n <i class=\"fas fa-info-circle me-1\"></i>\n {{ '::Document:MaxFileSize' | abpLocalization }}\n </small>\n </div>\n </div>\n</div>\n", styles: [".document-upload-card{border:1px solid var(--bs-border-color)}.cabinet-select{min-width:min(100%,18rem)}.drop-zone{border:2px dashed var(--bs-border-color, #dee2e6);border-radius:.5rem;background-color:var(--bs-tertiary-bg, #f8f9fa);transition:border-color .2s ease,background-color .2s ease,box-shadow .2s ease;min-height:24rem;display:flex;align-items:center;justify-content:center;padding:1.5rem}.drop-zone.drag-over{border-color:var(--bs-primary, #0d6efd);background-color:#0d6efd0d;box-shadow:0 0 0 .25rem #0d6efd14}.drop-zone.uploading,.drop-zone.has-results{cursor:not-allowed}.upload-icon{width:4rem;height:4rem;border-radius:.5rem;display:inline-flex;align-items:center;justify-content:center;background-color:#0d6efd14;color:var(--bs-primary, #0d6efd);font-size:1.8rem}.upload-queue{max-height:24rem;overflow:auto}.status-icon{width:1.5rem;height:1.5rem;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;font-size:.75rem}.min-width-0{min-width:0}\n"] }]
|
|
206
|
+
}], propDecorators: { browseButton: [{ type: i0.ViewChild, args: ['browseButton', { isSignal: true }] }] } });
|
|
207
|
+
|
|
208
|
+
class DocumentOverviewComponent {
|
|
209
|
+
constructor() {
|
|
210
|
+
this.permissionService = inject(PermissionService);
|
|
211
|
+
this.statisticsService = inject(DocumentStatisticsService);
|
|
212
|
+
this.cabinetService = inject(CabinetService);
|
|
213
|
+
this.documentTypeService = inject(DocumentTypeService);
|
|
214
|
+
this.destroyRef = inject(DestroyRef);
|
|
215
|
+
this.canUpload = this.permissionService.getGrantedPolicy(EXTRACT_PERMISSIONS.Documents.Upload);
|
|
216
|
+
this.canReview = this.permissionService.getGrantedPolicy(EXTRACT_PERMISSIONS.Documents.ConfirmClassification);
|
|
217
|
+
this.canViewCabinets = this.permissionService.getGrantedPolicy(EXTRACT_PERMISSIONS.Cabinets.Default);
|
|
218
|
+
this.canCreateCabinet = this.permissionService.getGrantedPolicy(EXTRACT_PERMISSIONS.Cabinets.Create);
|
|
219
|
+
this.canManageTypes = this.permissionService.getGrantedPolicy(EXTRACT_PERMISSIONS.DocumentTypes.Default);
|
|
220
|
+
this.canCreateType = this.permissionService.getGrantedPolicy(EXTRACT_PERMISSIONS.DocumentTypes.Create);
|
|
221
|
+
// Filtered-entry navigation (#335): cabinets and visible document types as quick links into the
|
|
222
|
+
// document list. No per-entity counts — navigation, not a dashboard. Cabinet list is gated by
|
|
223
|
+
// Cabinets.Default; document types are visible to any Documents.Default operator (GetVisible is
|
|
224
|
+
// decoupled from DocumentTypes.Default, #223).
|
|
225
|
+
this.cabinets = signal([], ...(ngDevMode ? [{ debugName: "cabinets" }] : /* istanbul ignore next */ []));
|
|
226
|
+
this.documentTypes = signal([], ...(ngDevMode ? [{ debugName: "documentTypes" }] : /* istanbul ignore next */ []));
|
|
227
|
+
// Loading starts true only when a fetch will actually run, so the empty state never flashes first.
|
|
228
|
+
this.cabinetsLoading = signal(this.canViewCabinets, ...(ngDevMode ? [{ debugName: "cabinetsLoading" }] : /* istanbul ignore next */ []));
|
|
229
|
+
this.typesLoading = signal(true, ...(ngDevMode ? [{ debugName: "typesLoading" }] : /* istanbul ignore next */ []));
|
|
230
|
+
// Show a section while it is still loading (avoids a layout pop), when it has items, or when the
|
|
231
|
+
// user can create the first one (actionable empty state). A plain viewer with neither items nor
|
|
232
|
+
// create rights sees nothing instead of a dead "empty" box.
|
|
233
|
+
this.showCabinetSection = computed(() => this.canViewCabinets &&
|
|
234
|
+
(this.cabinetsLoading() || this.cabinets().length > 0 || this.canCreateCabinet), ...(ngDevMode ? [{ debugName: "showCabinetSection" }] : /* istanbul ignore next */ []));
|
|
235
|
+
this.showTypeSection = computed(() => this.typesLoading() || this.documentTypes().length > 0 || this.canCreateType, ...(ngDevMode ? [{ debugName: "showTypeSection" }] : /* istanbul ignore next */ []));
|
|
236
|
+
this.stats = signal(null, ...(ngDevMode ? [{ debugName: "stats" }] : /* istanbul ignore next */ []));
|
|
237
|
+
this.statsLoading = signal(true, ...(ngDevMode ? [{ debugName: "statsLoading" }] : /* istanbul ignore next */ []));
|
|
238
|
+
this.statsError = signal(false, ...(ngDevMode ? [{ debugName: "statsError" }] : /* istanbul ignore next */ []));
|
|
239
|
+
// The loading skeleton must render the same number of tiles the data grid will, otherwise non-reviewers
|
|
240
|
+
// (who don't see the needs-review tile) get a 6 -> 5 layout jump when stats resolve.
|
|
241
|
+
this.skeletonSlots = this.canReview ? [0, 1, 2, 3, 4, 5] : [0, 1, 2, 3, 4];
|
|
242
|
+
// In-flight = stored-but-not-started (Uploaded) + actively processing. Composing the display bucket here
|
|
243
|
+
// keeps the API contract a faithful per-status projection (#333 decision: granularity in the DTO, grouping in the UI).
|
|
244
|
+
this.processingCount = computed(() => {
|
|
245
|
+
const s = this.stats();
|
|
246
|
+
return (s?.uploadedCount ?? 0) + (s?.processingCount ?? 0);
|
|
247
|
+
}, ...(ngDevMode ? [{ debugName: "processingCount" }] : /* istanbul ignore next */ []));
|
|
248
|
+
this.isEmpty = computed(() => (this.stats()?.totalCount ?? 0) === 0, ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
|
|
249
|
+
// Exposed so the template can format the storage tile.
|
|
250
|
+
this.formatBytes = formatBytes;
|
|
251
|
+
// A trigger drives loads through switchMap so a slower earlier request can never overwrite a newer one
|
|
252
|
+
// (e.g. rapid Refresh clicks): each emission cancels the previous in-flight GET. catchError keeps the
|
|
253
|
+
// stream alive across failures so later retries still work.
|
|
254
|
+
this.reload$ = new Subject();
|
|
255
|
+
this.reload$
|
|
256
|
+
.pipe(tap(() => {
|
|
257
|
+
this.statsLoading.set(true);
|
|
258
|
+
this.statsError.set(false);
|
|
259
|
+
}), switchMap(() => this.statisticsService.get().pipe(catchError(() => {
|
|
260
|
+
this.statsError.set(true);
|
|
261
|
+
this.statsLoading.set(false);
|
|
262
|
+
return EMPTY;
|
|
263
|
+
}))), takeUntilDestroyed(this.destroyRef))
|
|
264
|
+
.subscribe(stats => {
|
|
265
|
+
this.stats.set(stats);
|
|
266
|
+
this.statsLoading.set(false);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
ngOnInit() {
|
|
270
|
+
this.loadStatistics();
|
|
271
|
+
if (this.canViewCabinets) {
|
|
272
|
+
this.loadCabinets();
|
|
273
|
+
}
|
|
274
|
+
this.loadDocumentTypes();
|
|
275
|
+
}
|
|
276
|
+
loadStatistics() {
|
|
277
|
+
this.reload$.next();
|
|
278
|
+
}
|
|
279
|
+
loadCabinets() {
|
|
280
|
+
this.cabinetService
|
|
281
|
+
.getList()
|
|
282
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
283
|
+
.subscribe({
|
|
284
|
+
next: list => {
|
|
285
|
+
this.cabinets.set(list);
|
|
286
|
+
this.cabinetsLoading.set(false);
|
|
287
|
+
},
|
|
288
|
+
error: () => {
|
|
289
|
+
this.cabinets.set([]);
|
|
290
|
+
this.cabinetsLoading.set(false);
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
loadDocumentTypes() {
|
|
295
|
+
this.documentTypeService
|
|
296
|
+
.getVisible()
|
|
297
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
298
|
+
.subscribe({
|
|
299
|
+
next: list => {
|
|
300
|
+
this.documentTypes.set(list);
|
|
301
|
+
this.typesLoading.set(false);
|
|
302
|
+
},
|
|
303
|
+
error: () => {
|
|
304
|
+
this.documentTypes.set([]);
|
|
305
|
+
this.typesLoading.set(false);
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DocumentOverviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
310
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: DocumentOverviewComponent, isStandalone: true, selector: "lib-document-overview", ngImport: i0, template: "<div class=\"document-overview container-fluid py-4\">\n <div class=\"document-overview-shell\">\n <div class=\"mb-4\">\n <h1 class=\"h3 mb-2\">{{ '::Menu:Documents' | abpLocalization }}</h1>\n <p class=\"text-muted mb-0\">{{ '::Document:Home:Subtitle' | abpLocalization }}</p>\n </div>\n\n <!-- Overview statistics (#333): loading / error / empty / data states. -->\n <section class=\"mb-4\">\n <h2 class=\"h5 mb-3\">{{ '::Document:Home:Statistics:Title' | abpLocalization }}</h2>\n\n @if (statsLoading()) {\n <div class=\"row g-3\">\n @for (slot of skeletonSlots; track slot) {\n <div class=\"col-6 col-md-4 col-xl-2\">\n <div class=\"card shadow-sm h-100 stat-card placeholder-glow\">\n <div class=\"card-body\">\n <span class=\"placeholder col-5 d-block mb-2\"></span>\n <span class=\"placeholder col-8\"></span>\n </div>\n </div>\n </div>\n }\n </div>\n } @else if (statsError()) {\n <div\n class=\"alert alert-warning d-flex flex-wrap align-items-center justify-content-between gap-2 mb-0\"\n role=\"alert\"\n >\n <span>\n <i class=\"fas fa-exclamation-triangle me-2\"></i>\n {{ '::Document:Home:Statistics:LoadFailed' | abpLocalization }}\n </span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"loadStatistics()\">\n {{ '::Refresh' | abpLocalization }}\n </button>\n </div>\n } @else if (isEmpty()) {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center text-muted py-4\">\n <i class=\"fas fa-chart-bar d-block mb-2 fs-4\"></i>\n {{ '::Document:Home:Statistics:Empty' | abpLocalization }}\n </div>\n </div>\n } @else {\n <div class=\"row g-3\">\n <div class=\"col-6 col-md-4 col-xl-2\">\n <div class=\"card shadow-sm h-100 stat-card\">\n <div class=\"card-body\">\n <div class=\"stat-value\">{{ stats()?.totalCount ?? 0 }}</div>\n <div class=\"stat-label\">{{ '::Document:Home:Statistics:Total' | abpLocalization }}</div>\n </div>\n </div>\n </div>\n\n <div class=\"col-6 col-md-4 col-xl-2\">\n <div class=\"card shadow-sm h-100 stat-card\">\n <div class=\"card-body\">\n <div class=\"stat-value text-info\">{{ processingCount() }}</div>\n <div class=\"stat-label\">{{ '::Document:Home:Statistics:Processing' | abpLocalization }}</div>\n </div>\n </div>\n </div>\n\n <div class=\"col-6 col-md-4 col-xl-2\">\n <div class=\"card shadow-sm h-100 stat-card\">\n <div class=\"card-body\">\n <div class=\"stat-value text-success\">{{ stats()?.readyCount ?? 0 }}</div>\n <div class=\"stat-label\">{{ '::Document:Home:Statistics:Ready' | abpLocalization }}</div>\n </div>\n </div>\n </div>\n\n @if (canReview) {\n <div class=\"col-6 col-md-4 col-xl-2\">\n <a\n class=\"card shadow-sm h-100 stat-card text-decoration-none text-reset\"\n routerLink=\"/documents/list\"\n [queryParams]=\"{ review: 1 }\"\n >\n <div class=\"card-body\">\n <div class=\"stat-value text-warning\">{{ stats()?.needsReviewCount ?? 0 }}</div>\n <div class=\"stat-label\">{{ '::Document:Home:Statistics:NeedsReview' | abpLocalization }}</div>\n </div>\n </a>\n </div>\n }\n\n <div class=\"col-6 col-md-4 col-xl-2\">\n <div class=\"card shadow-sm h-100 stat-card\">\n <div class=\"card-body\">\n <div class=\"stat-value\" [class.text-danger]=\"(stats()?.failedCount ?? 0) > 0\">\n {{ stats()?.failedCount ?? 0 }}\n </div>\n <div class=\"stat-label\">{{ '::Document:Home:Statistics:Failed' | abpLocalization }}</div>\n </div>\n </div>\n </div>\n\n <div class=\"col-6 col-md-4 col-xl-2\">\n <div class=\"card shadow-sm h-100 stat-card\">\n <div class=\"card-body\">\n <div class=\"stat-value\">{{ formatBytes(stats()?.totalStorageBytes) }}</div>\n <div class=\"stat-label\">{{ '::Document:Home:Statistics:Storage' | abpLocalization }}</div>\n </div>\n </div>\n </div>\n </div>\n }\n </section>\n\n <div class=\"row g-4 align-items-stretch\">\n <div class=\"col-xl-8\">\n @if (canUpload) {\n <lib-document-upload />\n } @else {\n <div class=\"card shadow-sm h-100\">\n <div class=\"card-body p-4 p-lg-5 d-flex flex-column justify-content-center text-center\">\n <span class=\"readonly-icon mx-auto mb-3\">\n <i class=\"fas fa-lock\"></i>\n </span>\n <h2 class=\"h4 mb-2\">{{ '::Document:Home:ReadOnlyTitle' | abpLocalization }}</h2>\n <p class=\"text-muted mb-4\">{{ '::Document:Home:ReadOnlyMessage' | abpLocalization }}</p>\n <div>\n <a class=\"btn btn-primary\" routerLink=\"/documents/list\">\n <i class=\"fas fa-list me-1\"></i>\n {{ '::Menu:DocumentList' | abpLocalization }}\n </a>\n </div>\n </div>\n </div>\n }\n </div>\n\n <div class=\"col-xl-4\">\n <div class=\"d-flex flex-column gap-3 h-100\">\n <div class=\"card shadow-sm\">\n <div class=\"card-body p-4\">\n <h2 class=\"h5 mb-3\">{{ '::Document:Home:NextActions' | abpLocalization }}</h2>\n\n <div class=\"d-grid gap-2\">\n <a class=\"home-action\" routerLink=\"/documents/list\">\n <span class=\"home-action-icon text-bg-primary\"><i class=\"fas fa-list\"></i></span>\n <span>\n <span class=\"fw-semibold d-block\">{{ '::Menu:DocumentList' | abpLocalization }}</span>\n <small class=\"text-muted\">{{ '::Document:Home:DocumentListHint' | abpLocalization }}</small>\n </span>\n </a>\n\n @if (canReview) {\n <a class=\"home-action\" routerLink=\"/documents/list\" [queryParams]=\"{ review: 1 }\">\n <span class=\"home-action-icon text-bg-warning\"><i class=\"fas fa-clipboard-check\"></i></span>\n <span>\n <span class=\"fw-semibold d-block\">{{ '::Document:NeedsReview' | abpLocalization }}</span>\n <small class=\"text-muted\">{{ '::Document:Home:ReviewQueueHint' | abpLocalization }}</small>\n </span>\n </a>\n }\n\n @if (canViewCabinets) {\n <a class=\"home-action\" routerLink=\"/documents/cabinets\">\n <span class=\"home-action-icon text-bg-secondary\"><i class=\"fas fa-folder\"></i></span>\n <span>\n <span class=\"fw-semibold d-block\">{{ '::Menu:Cabinets' | abpLocalization }}</span>\n <small class=\"text-muted\">{{ '::Document:Home:CabinetsHint' | abpLocalization }}</small>\n </span>\n </a>\n }\n </div>\n </div>\n </div>\n\n <div class=\"card shadow-sm flex-grow-1\">\n <div class=\"card-body p-4\">\n <h2 class=\"h5 mb-3\">{{ '::Document:Home:ProcessingTitle' | abpLocalization }}</h2>\n <ol class=\"processing-list mb-0\">\n <li>{{ '::Document:Home:ProcessingStep:Upload' | abpLocalization }}</li>\n <li>{{ '::Document:Home:ProcessingStep:Extract' | abpLocalization }}</li>\n <li>{{ '::Document:Home:ProcessingStep:Review' | abpLocalization }}</li>\n </ol>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Filtered-entry navigation (#335): jump into the document list by cabinet or by document type.\n Secondary to upload, so it sits below the main row. No per-entity counts. -->\n @if (showCabinetSection()) {\n <section class=\"mt-4\">\n <div class=\"d-flex align-items-center justify-content-between mb-3\">\n <h2 class=\"h5 mb-0\">{{ '::Document:Home:CabinetsSection' | abpLocalization }}</h2>\n @if (cabinets().length > 0) {\n <a class=\"small\" routerLink=\"/documents/cabinets\">\n {{ '::Document:Home:ManageCabinets' | abpLocalization }}\n </a>\n }\n </div>\n\n @if (cabinets().length > 0) {\n <div class=\"home-chip-grid\">\n @for (cabinet of cabinets(); track cabinet.id) {\n <a\n class=\"home-chip\"\n routerLink=\"/documents/list\"\n [queryParams]=\"{ cabinetId: cabinet.id }\"\n >\n <i class=\"fas fa-folder text-warning\"></i>\n <span class=\"text-truncate\">{{ cabinet.name }}</span>\n </a>\n }\n </div>\n } @else if (!cabinetsLoading()) {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center text-muted py-4\">\n <i class=\"fas fa-folder-open d-block mb-2 fs-4\"></i>\n <div class=\"mb-2\">{{ '::Document:Home:CabinetsEmpty' | abpLocalization }}</div>\n @if (canCreateCabinet) {\n <a class=\"btn btn-sm btn-outline-primary\" routerLink=\"/documents/cabinets\">\n {{ '::Document:Home:CreateCabinet' | abpLocalization }}\n </a>\n }\n </div>\n </div>\n }\n </section>\n }\n\n @if (showTypeSection()) {\n <section class=\"mt-4\">\n <div class=\"d-flex align-items-center justify-content-between mb-3\">\n <h2 class=\"h5 mb-0\">{{ '::Document:Home:TypesSection' | abpLocalization }}</h2>\n @if (canManageTypes && documentTypes().length > 0) {\n <a class=\"small\" routerLink=\"/documents/types\">\n {{ '::Document:Home:ManageTypes' | abpLocalization }}\n </a>\n }\n </div>\n\n @if (documentTypes().length > 0) {\n <div class=\"home-chip-grid\">\n @for (type of documentTypes(); track type.id) {\n <a\n class=\"home-chip\"\n routerLink=\"/documents/list\"\n [queryParams]=\"{ documentTypeCode: type.typeCode }\"\n >\n <i class=\"fas fa-tags text-info\"></i>\n <span class=\"text-truncate\">{{ type.displayName }}</span>\n <code class=\"home-chip-code\">{{ type.typeCode }}</code>\n </a>\n }\n </div>\n } @else if (!typesLoading()) {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center text-muted py-4\">\n <i class=\"fas fa-tags d-block mb-2 fs-4\"></i>\n <div class=\"mb-2\">{{ '::Document:Home:TypesEmpty' | abpLocalization }}</div>\n @if (canCreateType) {\n <a class=\"btn btn-sm btn-outline-primary\" routerLink=\"/documents/types\">\n {{ '::Document:Home:CreateType' | abpLocalization }}\n </a>\n }\n </div>\n </div>\n }\n </section>\n }\n </div>\n</div>\n", styles: [".document-overview-shell{max-width:1320px;margin:0 auto}.readonly-icon,.home-action-icon{width:2.5rem;height:2.5rem;border-radius:.5rem;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto}.readonly-icon{background-color:var(--bs-tertiary-bg, #f8f9fa);color:var(--bs-secondary-color, #6c757d);font-size:1.25rem}.home-action{display:flex;gap:.75rem;align-items:flex-start;padding:.75rem;border:1px solid var(--bs-border-color);border-radius:.5rem;color:inherit;text-decoration:none;transition:border-color .2s ease,background-color .2s ease}.home-action:hover,.home-action:focus-visible{border-color:var(--bs-primary, #0d6efd);background-color:var(--bs-tertiary-bg, #f8f9fa)}.processing-list{padding-left:1.2rem;color:var(--bs-secondary-color, #6c757d)}.processing-list li+li{margin-top:.75rem}.stat-card .stat-value{font-size:1.5rem;font-weight:600;line-height:1.2}.stat-card .stat-label{margin-top:.25rem;font-size:.8125rem;color:var(--bs-secondary-color, #6c757d)}.home-chip-grid{display:flex;flex-wrap:wrap;gap:.5rem}.home-chip{display:inline-flex;align-items:center;gap:.5rem;max-width:100%;padding:.5rem .75rem;border:1px solid var(--bs-border-color);border-radius:.5rem;color:inherit;text-decoration:none;transition:border-color .2s ease,background-color .2s ease}.home-chip:hover,.home-chip:focus-visible{border-color:var(--bs-primary, #0d6efd);background-color:var(--bs-tertiary-bg, #f8f9fa)}.home-chip .home-chip-code{font-size:.75rem;color:var(--bs-secondary-color, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: DocumentUploadComponent, selector: "lib-document-upload" }, { kind: "pipe", type: LocalizationPipe, name: "abpLocalization" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
311
|
+
}
|
|
312
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DocumentOverviewComponent, decorators: [{
|
|
313
|
+
type: Component,
|
|
314
|
+
args: [{ selector: 'lib-document-overview', imports: [CommonModule, RouterModule, LocalizationPipe, DocumentUploadComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"document-overview container-fluid py-4\">\n <div class=\"document-overview-shell\">\n <div class=\"mb-4\">\n <h1 class=\"h3 mb-2\">{{ '::Menu:Documents' | abpLocalization }}</h1>\n <p class=\"text-muted mb-0\">{{ '::Document:Home:Subtitle' | abpLocalization }}</p>\n </div>\n\n <!-- Overview statistics (#333): loading / error / empty / data states. -->\n <section class=\"mb-4\">\n <h2 class=\"h5 mb-3\">{{ '::Document:Home:Statistics:Title' | abpLocalization }}</h2>\n\n @if (statsLoading()) {\n <div class=\"row g-3\">\n @for (slot of skeletonSlots; track slot) {\n <div class=\"col-6 col-md-4 col-xl-2\">\n <div class=\"card shadow-sm h-100 stat-card placeholder-glow\">\n <div class=\"card-body\">\n <span class=\"placeholder col-5 d-block mb-2\"></span>\n <span class=\"placeholder col-8\"></span>\n </div>\n </div>\n </div>\n }\n </div>\n } @else if (statsError()) {\n <div\n class=\"alert alert-warning d-flex flex-wrap align-items-center justify-content-between gap-2 mb-0\"\n role=\"alert\"\n >\n <span>\n <i class=\"fas fa-exclamation-triangle me-2\"></i>\n {{ '::Document:Home:Statistics:LoadFailed' | abpLocalization }}\n </span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"loadStatistics()\">\n {{ '::Refresh' | abpLocalization }}\n </button>\n </div>\n } @else if (isEmpty()) {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center text-muted py-4\">\n <i class=\"fas fa-chart-bar d-block mb-2 fs-4\"></i>\n {{ '::Document:Home:Statistics:Empty' | abpLocalization }}\n </div>\n </div>\n } @else {\n <div class=\"row g-3\">\n <div class=\"col-6 col-md-4 col-xl-2\">\n <div class=\"card shadow-sm h-100 stat-card\">\n <div class=\"card-body\">\n <div class=\"stat-value\">{{ stats()?.totalCount ?? 0 }}</div>\n <div class=\"stat-label\">{{ '::Document:Home:Statistics:Total' | abpLocalization }}</div>\n </div>\n </div>\n </div>\n\n <div class=\"col-6 col-md-4 col-xl-2\">\n <div class=\"card shadow-sm h-100 stat-card\">\n <div class=\"card-body\">\n <div class=\"stat-value text-info\">{{ processingCount() }}</div>\n <div class=\"stat-label\">{{ '::Document:Home:Statistics:Processing' | abpLocalization }}</div>\n </div>\n </div>\n </div>\n\n <div class=\"col-6 col-md-4 col-xl-2\">\n <div class=\"card shadow-sm h-100 stat-card\">\n <div class=\"card-body\">\n <div class=\"stat-value text-success\">{{ stats()?.readyCount ?? 0 }}</div>\n <div class=\"stat-label\">{{ '::Document:Home:Statistics:Ready' | abpLocalization }}</div>\n </div>\n </div>\n </div>\n\n @if (canReview) {\n <div class=\"col-6 col-md-4 col-xl-2\">\n <a\n class=\"card shadow-sm h-100 stat-card text-decoration-none text-reset\"\n routerLink=\"/documents/list\"\n [queryParams]=\"{ review: 1 }\"\n >\n <div class=\"card-body\">\n <div class=\"stat-value text-warning\">{{ stats()?.needsReviewCount ?? 0 }}</div>\n <div class=\"stat-label\">{{ '::Document:Home:Statistics:NeedsReview' | abpLocalization }}</div>\n </div>\n </a>\n </div>\n }\n\n <div class=\"col-6 col-md-4 col-xl-2\">\n <div class=\"card shadow-sm h-100 stat-card\">\n <div class=\"card-body\">\n <div class=\"stat-value\" [class.text-danger]=\"(stats()?.failedCount ?? 0) > 0\">\n {{ stats()?.failedCount ?? 0 }}\n </div>\n <div class=\"stat-label\">{{ '::Document:Home:Statistics:Failed' | abpLocalization }}</div>\n </div>\n </div>\n </div>\n\n <div class=\"col-6 col-md-4 col-xl-2\">\n <div class=\"card shadow-sm h-100 stat-card\">\n <div class=\"card-body\">\n <div class=\"stat-value\">{{ formatBytes(stats()?.totalStorageBytes) }}</div>\n <div class=\"stat-label\">{{ '::Document:Home:Statistics:Storage' | abpLocalization }}</div>\n </div>\n </div>\n </div>\n </div>\n }\n </section>\n\n <div class=\"row g-4 align-items-stretch\">\n <div class=\"col-xl-8\">\n @if (canUpload) {\n <lib-document-upload />\n } @else {\n <div class=\"card shadow-sm h-100\">\n <div class=\"card-body p-4 p-lg-5 d-flex flex-column justify-content-center text-center\">\n <span class=\"readonly-icon mx-auto mb-3\">\n <i class=\"fas fa-lock\"></i>\n </span>\n <h2 class=\"h4 mb-2\">{{ '::Document:Home:ReadOnlyTitle' | abpLocalization }}</h2>\n <p class=\"text-muted mb-4\">{{ '::Document:Home:ReadOnlyMessage' | abpLocalization }}</p>\n <div>\n <a class=\"btn btn-primary\" routerLink=\"/documents/list\">\n <i class=\"fas fa-list me-1\"></i>\n {{ '::Menu:DocumentList' | abpLocalization }}\n </a>\n </div>\n </div>\n </div>\n }\n </div>\n\n <div class=\"col-xl-4\">\n <div class=\"d-flex flex-column gap-3 h-100\">\n <div class=\"card shadow-sm\">\n <div class=\"card-body p-4\">\n <h2 class=\"h5 mb-3\">{{ '::Document:Home:NextActions' | abpLocalization }}</h2>\n\n <div class=\"d-grid gap-2\">\n <a class=\"home-action\" routerLink=\"/documents/list\">\n <span class=\"home-action-icon text-bg-primary\"><i class=\"fas fa-list\"></i></span>\n <span>\n <span class=\"fw-semibold d-block\">{{ '::Menu:DocumentList' | abpLocalization }}</span>\n <small class=\"text-muted\">{{ '::Document:Home:DocumentListHint' | abpLocalization }}</small>\n </span>\n </a>\n\n @if (canReview) {\n <a class=\"home-action\" routerLink=\"/documents/list\" [queryParams]=\"{ review: 1 }\">\n <span class=\"home-action-icon text-bg-warning\"><i class=\"fas fa-clipboard-check\"></i></span>\n <span>\n <span class=\"fw-semibold d-block\">{{ '::Document:NeedsReview' | abpLocalization }}</span>\n <small class=\"text-muted\">{{ '::Document:Home:ReviewQueueHint' | abpLocalization }}</small>\n </span>\n </a>\n }\n\n @if (canViewCabinets) {\n <a class=\"home-action\" routerLink=\"/documents/cabinets\">\n <span class=\"home-action-icon text-bg-secondary\"><i class=\"fas fa-folder\"></i></span>\n <span>\n <span class=\"fw-semibold d-block\">{{ '::Menu:Cabinets' | abpLocalization }}</span>\n <small class=\"text-muted\">{{ '::Document:Home:CabinetsHint' | abpLocalization }}</small>\n </span>\n </a>\n }\n </div>\n </div>\n </div>\n\n <div class=\"card shadow-sm flex-grow-1\">\n <div class=\"card-body p-4\">\n <h2 class=\"h5 mb-3\">{{ '::Document:Home:ProcessingTitle' | abpLocalization }}</h2>\n <ol class=\"processing-list mb-0\">\n <li>{{ '::Document:Home:ProcessingStep:Upload' | abpLocalization }}</li>\n <li>{{ '::Document:Home:ProcessingStep:Extract' | abpLocalization }}</li>\n <li>{{ '::Document:Home:ProcessingStep:Review' | abpLocalization }}</li>\n </ol>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Filtered-entry navigation (#335): jump into the document list by cabinet or by document type.\n Secondary to upload, so it sits below the main row. No per-entity counts. -->\n @if (showCabinetSection()) {\n <section class=\"mt-4\">\n <div class=\"d-flex align-items-center justify-content-between mb-3\">\n <h2 class=\"h5 mb-0\">{{ '::Document:Home:CabinetsSection' | abpLocalization }}</h2>\n @if (cabinets().length > 0) {\n <a class=\"small\" routerLink=\"/documents/cabinets\">\n {{ '::Document:Home:ManageCabinets' | abpLocalization }}\n </a>\n }\n </div>\n\n @if (cabinets().length > 0) {\n <div class=\"home-chip-grid\">\n @for (cabinet of cabinets(); track cabinet.id) {\n <a\n class=\"home-chip\"\n routerLink=\"/documents/list\"\n [queryParams]=\"{ cabinetId: cabinet.id }\"\n >\n <i class=\"fas fa-folder text-warning\"></i>\n <span class=\"text-truncate\">{{ cabinet.name }}</span>\n </a>\n }\n </div>\n } @else if (!cabinetsLoading()) {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center text-muted py-4\">\n <i class=\"fas fa-folder-open d-block mb-2 fs-4\"></i>\n <div class=\"mb-2\">{{ '::Document:Home:CabinetsEmpty' | abpLocalization }}</div>\n @if (canCreateCabinet) {\n <a class=\"btn btn-sm btn-outline-primary\" routerLink=\"/documents/cabinets\">\n {{ '::Document:Home:CreateCabinet' | abpLocalization }}\n </a>\n }\n </div>\n </div>\n }\n </section>\n }\n\n @if (showTypeSection()) {\n <section class=\"mt-4\">\n <div class=\"d-flex align-items-center justify-content-between mb-3\">\n <h2 class=\"h5 mb-0\">{{ '::Document:Home:TypesSection' | abpLocalization }}</h2>\n @if (canManageTypes && documentTypes().length > 0) {\n <a class=\"small\" routerLink=\"/documents/types\">\n {{ '::Document:Home:ManageTypes' | abpLocalization }}\n </a>\n }\n </div>\n\n @if (documentTypes().length > 0) {\n <div class=\"home-chip-grid\">\n @for (type of documentTypes(); track type.id) {\n <a\n class=\"home-chip\"\n routerLink=\"/documents/list\"\n [queryParams]=\"{ documentTypeCode: type.typeCode }\"\n >\n <i class=\"fas fa-tags text-info\"></i>\n <span class=\"text-truncate\">{{ type.displayName }}</span>\n <code class=\"home-chip-code\">{{ type.typeCode }}</code>\n </a>\n }\n </div>\n } @else if (!typesLoading()) {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center text-muted py-4\">\n <i class=\"fas fa-tags d-block mb-2 fs-4\"></i>\n <div class=\"mb-2\">{{ '::Document:Home:TypesEmpty' | abpLocalization }}</div>\n @if (canCreateType) {\n <a class=\"btn btn-sm btn-outline-primary\" routerLink=\"/documents/types\">\n {{ '::Document:Home:CreateType' | abpLocalization }}\n </a>\n }\n </div>\n </div>\n }\n </section>\n }\n </div>\n</div>\n", styles: [".document-overview-shell{max-width:1320px;margin:0 auto}.readonly-icon,.home-action-icon{width:2.5rem;height:2.5rem;border-radius:.5rem;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto}.readonly-icon{background-color:var(--bs-tertiary-bg, #f8f9fa);color:var(--bs-secondary-color, #6c757d);font-size:1.25rem}.home-action{display:flex;gap:.75rem;align-items:flex-start;padding:.75rem;border:1px solid var(--bs-border-color);border-radius:.5rem;color:inherit;text-decoration:none;transition:border-color .2s ease,background-color .2s ease}.home-action:hover,.home-action:focus-visible{border-color:var(--bs-primary, #0d6efd);background-color:var(--bs-tertiary-bg, #f8f9fa)}.processing-list{padding-left:1.2rem;color:var(--bs-secondary-color, #6c757d)}.processing-list li+li{margin-top:.75rem}.stat-card .stat-value{font-size:1.5rem;font-weight:600;line-height:1.2}.stat-card .stat-label{margin-top:.25rem;font-size:.8125rem;color:var(--bs-secondary-color, #6c757d)}.home-chip-grid{display:flex;flex-wrap:wrap;gap:.5rem}.home-chip{display:inline-flex;align-items:center;gap:.5rem;max-width:100%;padding:.5rem .75rem;border:1px solid var(--bs-border-color);border-radius:.5rem;color:inherit;text-decoration:none;transition:border-color .2s ease,background-color .2s ease}.home-chip:hover,.home-chip:focus-visible{border-color:var(--bs-primary, #0d6efd);background-color:var(--bs-tertiary-bg, #f8f9fa)}.home-chip .home-chip-code{font-size:.75rem;color:var(--bs-secondary-color, #6c757d)}\n"] }]
|
|
315
|
+
}], ctorParameters: () => [] });
|
|
316
|
+
|
|
317
|
+
export { DocumentOverviewComponent };
|
|
318
|
+
//# sourceMappingURL=dignite-vault-extract-documents-document-overview.component-BHUUUIVr.mjs.map
|