@maro-tech/card 0.0.2 → 0.0.4

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.
@@ -1,10 +1,20 @@
1
1
  import * as i1 from '@angular/common';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { input, inject, DestroyRef, signal, Component, computed, output, effect, HostListener, InjectionToken, forwardRef } from '@angular/core';
5
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
- import { EventBus, UiButtonComponent } from '@maro-tech/core';
4
+ import { input, inject, DestroyRef, signal, Component, computed, output, effect, HostListener, InjectionToken, forwardRef, Injectable, ViewChild, untracked, Input } from '@angular/core';
5
+ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
6
+ import { EventBus, UiButtonComponent, UiToastService, UiModalComponent, UiConfirmService } from '@maro-tech/core';
7
7
  import { ActivatedRoute, Router } from '@angular/router';
8
+ import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
9
+ import { shareReplay, forkJoin, switchMap, timer, of, map as map$1 } from 'rxjs';
10
+ import { map, exhaustMap, catchError, takeWhile, takeUntil, finalize } from 'rxjs/operators';
11
+ import * as i1$2 from 'lucide-angular';
12
+ import { LucideAngularModule, Eye, FileText, Plus, Sparkles, Trash } from 'lucide-angular';
13
+ import * as i1$1 from '@angular/forms';
14
+ import { FormsModule } from '@angular/forms';
15
+ import { UiSelectComponent, UiActionFormModalComponent } from '@maro-tech/form';
16
+ import { UiTableComponent } from '@maro-tech/table';
17
+ import JsBarcode from 'jsbarcode';
8
18
 
9
19
  const CARD_ACTION_REQUESTED = 'card.action.requested';
10
20
  const CARD_ACTION_STARTED = 'card.action.started';
@@ -604,9 +614,1679 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
604
614
 
605
615
  const UI_ENTITY_CARD_HOST = new InjectionToken('UI_ENTITY_CARD_HOST');
606
616
 
617
+ const UI_CARD_API_BASE_URL = new InjectionToken('UI_CARD_API_BASE_URL', { factory: () => '' });
618
+ const apiUrl = (baseUrl, path) => `${String(baseUrl || '').replace(/\/$/, '')}${path}`;
619
+ class CardDictionaryService {
620
+ http = inject(HttpClient);
621
+ baseUrl = inject(UI_CARD_API_BASE_URL);
622
+ getSuggestByName(name) {
623
+ return this.http.get(apiUrl(this.baseUrl, `/api/suggest/${encodeURIComponent(name)}`));
624
+ }
625
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardDictionaryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
626
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardDictionaryService, providedIn: 'root' });
627
+ }
628
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardDictionaryService, decorators: [{
629
+ type: Injectable,
630
+ args: [{ providedIn: 'root' }]
631
+ }] });
632
+ class CardCurrentUserService {
633
+ http = inject(HttpClient);
634
+ baseUrl = inject(UI_CARD_API_BASE_URL);
635
+ user$ = this.http.get(apiUrl(this.baseUrl, '/api/users/me'), { withCredentials: true }).pipe(shareReplay(1));
636
+ getCurrentUser() { return this.user$; }
637
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardCurrentUserService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
638
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardCurrentUserService, providedIn: 'root' });
639
+ }
640
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardCurrentUserService, decorators: [{
641
+ type: Injectable,
642
+ args: [{ providedIn: 'root' }]
643
+ }] });
644
+ class CardImageService {
645
+ baseUrl = inject(UI_CARD_API_BASE_URL);
646
+ getPhotoLink(id) {
647
+ return id ? apiUrl(this.baseUrl, `/api/files/${encodeURIComponent(id)}/preview?scale=25`) : undefined;
648
+ }
649
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
650
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardImageService, providedIn: 'root' });
651
+ }
652
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardImageService, decorators: [{
653
+ type: Injectable,
654
+ args: [{ providedIn: 'root' }]
655
+ }] });
656
+ const cardApiResourceUrl = (baseUrl, path) => apiUrl(baseUrl, path);
657
+ const cardApiParams = (params) => Object.entries(params).reduce((result, [key, value]) => value == null ? result : result.set(key, String(value)), new HttpParams());
658
+
659
+ class UiEntityDocumentsService {
660
+ http = inject(HttpClient);
661
+ baseUrl = inject(UI_CARD_API_BASE_URL);
662
+ list(entity, entityId) {
663
+ const params = cardApiParams({ entity, entityId });
664
+ return this.http.get(cardApiResourceUrl(this.baseUrl, '/api/entity-files'), { params });
665
+ }
666
+ recognitionStatus(id) {
667
+ return this.http.get(cardApiResourceUrl(this.baseUrl, `/api/entity-files/${encodeURIComponent(id)}/recognition-status`));
668
+ }
669
+ upload(file) {
670
+ const formData = new FormData();
671
+ formData.append('file', file);
672
+ formData.append('fileName', file.name);
673
+ return this.http.post(cardApiResourceUrl(this.baseUrl, '/api/files'), formData);
674
+ }
675
+ attach(payload) {
676
+ return this.http.post(cardApiResourceUrl(this.baseUrl, '/api/entity-files'), payload);
677
+ }
678
+ recognize(id) {
679
+ return this.http.post(cardApiResourceUrl(this.baseUrl, `/api/entity-files/${encodeURIComponent(id)}/recognize`), {});
680
+ }
681
+ rename(id, name) {
682
+ return this.http.patch(cardApiResourceUrl(this.baseUrl, `/api/entity-files/${encodeURIComponent(id)}`), { name });
683
+ }
684
+ remove(id) {
685
+ return this.http.delete(cardApiResourceUrl(this.baseUrl, `/api/entity-files/${encodeURIComponent(id)}`));
686
+ }
687
+ resolveDownloadUrl(fileId) {
688
+ return cardApiResourceUrl(this.baseUrl, `/api/files/${encodeURIComponent(fileId)}`);
689
+ }
690
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: UiEntityDocumentsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
691
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: UiEntityDocumentsService, providedIn: 'root' });
692
+ }
693
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: UiEntityDocumentsService, decorators: [{
694
+ type: Injectable,
695
+ args: [{ providedIn: 'root' }]
696
+ }] });
697
+
698
+ class DocumentsUploadModalComponent {
699
+ isOpen = input(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
700
+ entity = input('', ...(ngDevMode ? [{ debugName: "entity" }] : /* istanbul ignore next */ []));
701
+ id = input('', ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
702
+ closed = output();
703
+ saved = output();
704
+ documentTypeId = signal(null, ...(ngDevMode ? [{ debugName: "documentTypeId" }] : /* istanbul ignore next */ []));
705
+ fileNames = signal([], ...(ngDevMode ? [{ debugName: "fileNames" }] : /* istanbul ignore next */ []));
706
+ isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : /* istanbul ignore next */ []));
707
+ uploadFileInput;
708
+ documentsService = inject(UiEntityDocumentsService);
709
+ toast = inject(UiToastService);
710
+ uploadFiles = [];
711
+ isOpenEffect = effect(() => {
712
+ if (!this.isOpen()) {
713
+ this.resetState();
714
+ }
715
+ }, ...(ngDevMode ? [{ debugName: "isOpenEffect" }] : /* istanbul ignore next */ []));
716
+ close() {
717
+ this.closed.emit();
718
+ }
719
+ onFilesSelected(event) {
720
+ const input = event.target;
721
+ const files = Array.from(input?.files ?? []);
722
+ this.uploadFiles = files;
723
+ this.fileNames.set(files.map((file) => file.name));
724
+ }
725
+ onDocumentTypeChange(value) {
726
+ this.documentTypeId.set(Array.isArray(value) ? value[0] ?? null : value);
727
+ }
728
+ submitUpload() {
729
+ const entity = String(this.entity() || '').trim();
730
+ const id = String(this.id() || '').trim();
731
+ if (!entity || !id) {
732
+ this.toast.show('Немає entity або id запису', 'warning', 2400);
733
+ return;
734
+ }
735
+ if (!this.uploadFiles.length) {
736
+ this.toast.show('Оберіть файл для завантаження', 'warning', 2400);
737
+ return;
738
+ }
739
+ this.isUploading.set(true);
740
+ forkJoin(this.uploadFiles.map((file) => this.documentsService.upload(file).pipe(switchMap((result) => this.documentsService.attach({
741
+ entity,
742
+ entityId: id,
743
+ fileId: result.id,
744
+ documentTypeId: this.documentTypeId(),
745
+ }))))).subscribe({
746
+ next: () => {
747
+ this.toast.show(this.uploadFiles.length > 1 ? `Додано документів: ${this.uploadFiles.length}` : 'Документ додано', 'success', 2200);
748
+ this.isUploading.set(false);
749
+ this.saved.emit();
750
+ this.closed.emit();
751
+ },
752
+ error: (error) => {
753
+ this.toast.show(error?.error?.message || 'Не вдалося додати документи', 'error', 3200);
754
+ this.isUploading.set(false);
755
+ },
756
+ });
757
+ }
758
+ resetState() {
759
+ this.documentTypeId.set(null);
760
+ this.fileNames.set([]);
761
+ this.isUploading.set(false);
762
+ this.uploadFiles = [];
763
+ if (this.uploadFileInput) {
764
+ this.uploadFileInput.nativeElement.value = '';
765
+ }
766
+ }
767
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DocumentsUploadModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
768
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: DocumentsUploadModalComponent, isStandalone: true, selector: "app-documents-upload-modal", inputs: { isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, entity: { classPropertyName: "entity", publicName: "entity", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", saved: "saved" }, viewQueries: [{ propertyName: "uploadFileInput", first: true, predicate: ["uploadFileInput"], descendants: true }], ngImport: i0, template: "<core-modal\n title=\"\u0414\u043E\u0434\u0430\u0442\u0438 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\"\n [isOpen]=\"isOpen()\"\n (closed)=\"close()\"\n>\n <div class=\"space-y-4\">\n <forms-select\n [data]=\"'doc_type'\"\n name=\"uploadDocumentTypeId\"\n label=\"\u0422\u0438\u043F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\"\n hint=\"\u041D\u0435\u043E\u0431\u043E\u0432'\u044F\u0437\u043A\u043E\u0432\u043E\"\n [ngModel]=\"documentTypeId()\"\n (ngModelChange)=\"onDocumentTypeChange($event)\"\n placeholder=\"\u0422\u0438\u043F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\"\n [fullWidth]=\"true\"\n [searchable]=\"true\"\n ></forms-select>\n\n <div class=\"space-y-2\">\n <label class=\"block text-sm font-medium text-slate-700\" for=\"document-files\">\u0424\u0430\u0439\u043B\u0438</label>\n <input\n #uploadFileInput\n id=\"document-files\"\n type=\"file\"\n class=\"block w-full rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-sm text-slate-900 outline-none transition file:mr-3 file:rounded-lg file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white focus:border-slate-400 focus:ring-2 focus:ring-slate-200\"\n multiple\n (change)=\"onFilesSelected($event)\"\n />\n @if (fileNames().length) {\n <div class=\"rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-600\">\n @for (fileName of fileNames(); track fileName) {\n <div class=\"truncate\">{{ fileName }}</div>\n }\n </div>\n } @else {\n <p class=\"text-xs text-slate-500\">\u041C\u043E\u0436\u043D\u0430 \u043E\u0431\u0440\u0430\u0442\u0438 \u043E\u0434\u0438\u043D \u0430\u0431\u043E \u0434\u0435\u043A\u0456\u043B\u044C\u043A\u0430 \u0444\u0430\u0439\u043B\u0456\u0432.</p>\n }\n </div>\n </div>\n\n <div slot=\"footer\" class=\"flex justify-end gap-2\">\n <ui-button variant=\"ghost\" [disabled]=\"isUploading()\" (click)=\"close()\">\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438</ui-button>\n <ui-button variant=\"primary\" [disabled]=\"isUploading()\" (click)=\"submitUpload()\">\n {{ isUploading() ? '\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F...' : '\u0417\u0431\u0435\u0440\u0435\u0433\u0442\u0438' }}\n </ui-button>\n </div>\n</core-modal>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: UiModalComponent, selector: "core-modal", inputs: ["isOpen", "title", "size", "position", "closable", "closeOnBackdrop", "closeOnEscape"], outputs: ["closed"] }, { kind: "component", type: UiSelectComponent, selector: "forms-select", inputs: ["label", "placeholder", "name", "data", "required", "fullWidth", "disabled", "multiple", "includeBlankOption", "blankOptionLabel", "emptyStateLabel", "searchable", "remoteSearch", "searchPlaceholder", "options", "error", "hint", "triggerClass", "panelClass"], outputs: ["valueChange", "searchChange"] }] });
769
+ }
770
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DocumentsUploadModalComponent, decorators: [{
771
+ type: Component,
772
+ args: [{ selector: 'app-documents-upload-modal', standalone: true, imports: [CommonModule, FormsModule, LucideAngularModule, UiButtonComponent, UiModalComponent, UiSelectComponent], template: "<core-modal\n title=\"\u0414\u043E\u0434\u0430\u0442\u0438 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\"\n [isOpen]=\"isOpen()\"\n (closed)=\"close()\"\n>\n <div class=\"space-y-4\">\n <forms-select\n [data]=\"'doc_type'\"\n name=\"uploadDocumentTypeId\"\n label=\"\u0422\u0438\u043F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\"\n hint=\"\u041D\u0435\u043E\u0431\u043E\u0432'\u044F\u0437\u043A\u043E\u0432\u043E\"\n [ngModel]=\"documentTypeId()\"\n (ngModelChange)=\"onDocumentTypeChange($event)\"\n placeholder=\"\u0422\u0438\u043F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\"\n [fullWidth]=\"true\"\n [searchable]=\"true\"\n ></forms-select>\n\n <div class=\"space-y-2\">\n <label class=\"block text-sm font-medium text-slate-700\" for=\"document-files\">\u0424\u0430\u0439\u043B\u0438</label>\n <input\n #uploadFileInput\n id=\"document-files\"\n type=\"file\"\n class=\"block w-full rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-sm text-slate-900 outline-none transition file:mr-3 file:rounded-lg file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white focus:border-slate-400 focus:ring-2 focus:ring-slate-200\"\n multiple\n (change)=\"onFilesSelected($event)\"\n />\n @if (fileNames().length) {\n <div class=\"rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-600\">\n @for (fileName of fileNames(); track fileName) {\n <div class=\"truncate\">{{ fileName }}</div>\n }\n </div>\n } @else {\n <p class=\"text-xs text-slate-500\">\u041C\u043E\u0436\u043D\u0430 \u043E\u0431\u0440\u0430\u0442\u0438 \u043E\u0434\u0438\u043D \u0430\u0431\u043E \u0434\u0435\u043A\u0456\u043B\u044C\u043A\u0430 \u0444\u0430\u0439\u043B\u0456\u0432.</p>\n }\n </div>\n </div>\n\n <div slot=\"footer\" class=\"flex justify-end gap-2\">\n <ui-button variant=\"ghost\" [disabled]=\"isUploading()\" (click)=\"close()\">\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438</ui-button>\n <ui-button variant=\"primary\" [disabled]=\"isUploading()\" (click)=\"submitUpload()\">\n {{ isUploading() ? '\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F...' : '\u0417\u0431\u0435\u0440\u0435\u0433\u0442\u0438' }}\n </ui-button>\n </div>\n</core-modal>\n" }]
773
+ }], propDecorators: { isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }], entity: [{ type: i0.Input, args: [{ isSignal: true, alias: "entity", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], saved: [{ type: i0.Output, args: ["saved"] }], uploadFileInput: [{
774
+ type: ViewChild,
775
+ args: ['uploadFileInput']
776
+ }] } });
777
+
778
+ class DocumentsTextModalComponent {
779
+ isOpen = input(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
780
+ document = input(null, ...(ngDevMode ? [{ debugName: "document" }] : /* istanbul ignore next */ []));
781
+ closed = output();
782
+ recognizeRequested = output();
783
+ close() {
784
+ this.closed.emit();
785
+ }
786
+ requestRecognize() {
787
+ this.recognizeRequested.emit();
788
+ }
789
+ isImage() {
790
+ return String(this.document()?.type || '').toLowerCase().startsWith('image/');
791
+ }
792
+ hasDocumentHtml() {
793
+ return !!String(this.document()?.text || '').trim();
794
+ }
795
+ getDocumentHtml() {
796
+ const text = String(this.document()?.text || '').trim();
797
+ if (!text) {
798
+ return '';
799
+ }
800
+ if (this.looksLikeHtml(text)) {
801
+ return text;
802
+ }
803
+ return this.escapeHtml(text).replace(/\r?\n/g, '<br>');
804
+ }
805
+ looksLikeHtml(value) {
806
+ return /<\/?[a-z][\s\S]*>/i.test(value);
807
+ }
808
+ escapeHtml(value) {
809
+ return value
810
+ .replace(/&/g, '&amp;')
811
+ .replace(/</g, '&lt;')
812
+ .replace(/>/g, '&gt;')
813
+ .replace(/"/g, '&quot;')
814
+ .replace(/'/g, '&#39;');
815
+ }
816
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DocumentsTextModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
817
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: DocumentsTextModalComponent, isStandalone: true, selector: "app-documents-text-modal", inputs: { isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, document: { classPropertyName: "document", publicName: "document", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", recognizeRequested: "recognizeRequested" }, ngImport: i0, template: "<core-modal\n title=\"\u0420\u043E\u0437\u043F\u0456\u0437\u043D\u0430\u043D\u0456 \u0434\u0430\u043D\u0456\"\n size=\"lg\"\n [isOpen]=\"isOpen()\"\n (closed)=\"close()\"\n>\n <div class=\"space-y-3\">\n @if (hasDocumentHtml()) {\n <div\n class=\"max-h-[32rem] overflow-auto rounded-xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm text-slate-900\"\n [innerHTML]=\"getDocumentHtml()\"\n ></div>\n } @else {\n <div class=\"rounded-xl border border-dashed border-slate-200 bg-slate-50 px-4 py-6 text-sm text-slate-500\">\n \u0414\u0430\u043D\u0456 \u0449\u0435 \u043D\u0435 \u0440\u043E\u0437\u043F\u0456\u0437\u043D\u0430\u043D\u0456. \u041D\u0430\u0442\u0438\u0441\u043D\u0438 \u201C\u0420\u043E\u0437\u043F\u0456\u0437\u043D\u0430\u0442\u0438 \u0434\u0430\u043D\u0456\u201D.\n </div>\n }\n </div>\n\n <div slot=\"footer\" class=\"flex justify-end gap-2\">\n @if (isImage()) {\n <ui-button variant=\"secondary\" (click)=\"requestRecognize()\">\u0420\u043E\u0437\u043F\u0456\u0437\u043D\u0430\u0442\u0438 \u0449\u0435 \u0440\u0430\u0437</ui-button>\n }\n <ui-button variant=\"primary\" (click)=\"close()\">\u0417\u0430\u043A\u0440\u0438\u0442\u0438</ui-button>\n </div>\n</core-modal>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: UiModalComponent, selector: "core-modal", inputs: ["isOpen", "title", "size", "position", "closable", "closeOnBackdrop", "closeOnEscape"], outputs: ["closed"] }] });
818
+ }
819
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DocumentsTextModalComponent, decorators: [{
820
+ type: Component,
821
+ args: [{ selector: 'app-documents-text-modal', standalone: true, imports: [CommonModule, UiButtonComponent, UiModalComponent], template: "<core-modal\n title=\"\u0420\u043E\u0437\u043F\u0456\u0437\u043D\u0430\u043D\u0456 \u0434\u0430\u043D\u0456\"\n size=\"lg\"\n [isOpen]=\"isOpen()\"\n (closed)=\"close()\"\n>\n <div class=\"space-y-3\">\n @if (hasDocumentHtml()) {\n <div\n class=\"max-h-[32rem] overflow-auto rounded-xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm text-slate-900\"\n [innerHTML]=\"getDocumentHtml()\"\n ></div>\n } @else {\n <div class=\"rounded-xl border border-dashed border-slate-200 bg-slate-50 px-4 py-6 text-sm text-slate-500\">\n \u0414\u0430\u043D\u0456 \u0449\u0435 \u043D\u0435 \u0440\u043E\u0437\u043F\u0456\u0437\u043D\u0430\u043D\u0456. \u041D\u0430\u0442\u0438\u0441\u043D\u0438 \u201C\u0420\u043E\u0437\u043F\u0456\u0437\u043D\u0430\u0442\u0438 \u0434\u0430\u043D\u0456\u201D.\n </div>\n }\n </div>\n\n <div slot=\"footer\" class=\"flex justify-end gap-2\">\n @if (isImage()) {\n <ui-button variant=\"secondary\" (click)=\"requestRecognize()\">\u0420\u043E\u0437\u043F\u0456\u0437\u043D\u0430\u0442\u0438 \u0449\u0435 \u0440\u0430\u0437</ui-button>\n }\n <ui-button variant=\"primary\" (click)=\"close()\">\u0417\u0430\u043A\u0440\u0438\u0442\u0438</ui-button>\n </div>\n</core-modal>\n" }]
822
+ }], propDecorators: { isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }], document: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], recognizeRequested: [{ type: i0.Output, args: ["recognizeRequested"] }] } });
823
+
824
+ class DocumentsImagePreviewModalComponent {
825
+ isOpen = input(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
826
+ document = input(null, ...(ngDevMode ? [{ debugName: "document" }] : /* istanbul ignore next */ []));
827
+ closed = output();
828
+ close() {
829
+ this.closed.emit();
830
+ }
831
+ getPreviewUrl(url) {
832
+ const match = String(url || '').match(/^(.*\/api\/files\/[^/?#]+)(?:\?.*)?$/i);
833
+ return match ? `${match[1]}/preview?scale=25` : url;
834
+ }
835
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DocumentsImagePreviewModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
836
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: DocumentsImagePreviewModalComponent, isStandalone: true, selector: "app-documents-image-preview-modal", inputs: { isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, document: { classPropertyName: "document", publicName: "document", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, ngImport: i0, template: "<core-modal\n [title]=\"document()?.name || '\u041F\u0435\u0440\u0435\u0433\u043B\u044F\u0434 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430'\"\n size=\"lg\"\n [isOpen]=\"isOpen()\"\n (closed)=\"close()\"\n>\n @if (document()) {\n <div class=\"overflow-hidden rounded-2xl bg-slate-100\">\n <img\n [src]=\"getPreviewUrl(document()!.downloadUrl)\"\n [alt]=\"document()!.name || '\u0414\u043E\u043A\u0443\u043C\u0435\u043D\u0442'\"\n decoding=\"async\"\n class=\"max-h-[75vh] w-full object-contain\"\n >\n </div>\n }\n\n <div slot=\"footer\" class=\"flex justify-end gap-2\">\n <ui-button variant=\"primary\" (click)=\"close()\">\u0417\u0430\u043A\u0440\u0438\u0442\u0438</ui-button>\n </div>\n</core-modal>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: UiModalComponent, selector: "core-modal", inputs: ["isOpen", "title", "size", "position", "closable", "closeOnBackdrop", "closeOnEscape"], outputs: ["closed"] }] });
837
+ }
838
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DocumentsImagePreviewModalComponent, decorators: [{
839
+ type: Component,
840
+ args: [{ selector: 'app-documents-image-preview-modal', standalone: true, imports: [CommonModule, UiButtonComponent, UiModalComponent], template: "<core-modal\n [title]=\"document()?.name || '\u041F\u0435\u0440\u0435\u0433\u043B\u044F\u0434 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430'\"\n size=\"lg\"\n [isOpen]=\"isOpen()\"\n (closed)=\"close()\"\n>\n @if (document()) {\n <div class=\"overflow-hidden rounded-2xl bg-slate-100\">\n <img\n [src]=\"getPreviewUrl(document()!.downloadUrl)\"\n [alt]=\"document()!.name || '\u0414\u043E\u043A\u0443\u043C\u0435\u043D\u0442'\"\n decoding=\"async\"\n class=\"max-h-[75vh] w-full object-contain\"\n >\n </div>\n }\n\n <div slot=\"footer\" class=\"flex justify-end gap-2\">\n <ui-button variant=\"primary\" (click)=\"close()\">\u0417\u0430\u043A\u0440\u0438\u0442\u0438</ui-button>\n </div>\n</core-modal>\n" }]
841
+ }], propDecorators: { isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }], document: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
842
+
843
+ const DOCUMENT_TYPE_DICTIONARY = 'doc_type';
844
+ const RECOGNITION_POLLING_TIMEOUT_MS = 5 * 60 * 1000;
845
+ class DocumentsComponent {
846
+ entity = input('', ...(ngDevMode ? [{ debugName: "entity" }] : /* istanbul ignore next */ []));
847
+ id = input('', ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
848
+ documentTypeDictionary = DOCUMENT_TYPE_DICTIONARY;
849
+ documents = signal([], ...(ngDevMode ? [{ debugName: "documents" }] : /* istanbul ignore next */ []));
850
+ documentTypeOptions = signal([], ...(ngDevMode ? [{ debugName: "documentTypeOptions" }] : /* istanbul ignore next */ []));
851
+ isUploadOpen = signal(false, ...(ngDevMode ? [{ debugName: "isUploadOpen" }] : /* istanbul ignore next */ []));
852
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
853
+ recognizingDocumentId = signal(null, ...(ngDevMode ? [{ debugName: "recognizingDocumentId" }] : /* istanbul ignore next */ []));
854
+ isTextOpen = signal(false, ...(ngDevMode ? [{ debugName: "isTextOpen" }] : /* istanbul ignore next */ []));
855
+ isImagePreviewOpen = signal(false, ...(ngDevMode ? [{ debugName: "isImagePreviewOpen" }] : /* istanbul ignore next */ []));
856
+ textTarget = signal(null, ...(ngDevMode ? [{ debugName: "textTarget" }] : /* istanbul ignore next */ []));
857
+ imagePreviewTarget = signal(null, ...(ngDevMode ? [{ debugName: "imagePreviewTarget" }] : /* istanbul ignore next */ []));
858
+ deletingDocumentId = signal(null, ...(ngDevMode ? [{ debugName: "deletingDocumentId" }] : /* istanbul ignore next */ []));
859
+ eyeIcon = Eye;
860
+ fileIcon = FileText;
861
+ plusIcon = Plus;
862
+ sparkleIcon = Sparkles;
863
+ deleteIcon = Trash;
864
+ isAdmin = toSignal(inject(CardCurrentUserService).getCurrentUser().pipe(map((user) => String(user.role || '').trim().toUpperCase() === 'ADMIN')), { initialValue: false });
865
+ documentsService = inject(UiEntityDocumentsService);
866
+ dictionaryService = inject(CardDictionaryService);
867
+ toast = inject(UiToastService);
868
+ confirmService = inject(UiConfirmService);
869
+ destroyRef = inject(DestroyRef);
870
+ loadedDocumentTypeDictionary = '';
871
+ recognitionPolling;
872
+ reloadEffect = effect(() => {
873
+ this.entity();
874
+ this.id();
875
+ this.loadDocuments();
876
+ }, ...(ngDevMode ? [{ debugName: "reloadEffect" }] : /* istanbul ignore next */ []));
877
+ documentTypeDictionaryEffect = effect(() => {
878
+ const dictionaryName = this.documentTypeDictionary.trim();
879
+ if (!dictionaryName) {
880
+ this.loadedDocumentTypeDictionary = '';
881
+ this.documentTypeOptions.set([]);
882
+ return;
883
+ }
884
+ if (dictionaryName === this.loadedDocumentTypeDictionary) {
885
+ return;
886
+ }
887
+ this.loadedDocumentTypeDictionary = dictionaryName;
888
+ this.dictionaryService.getSuggestByName(dictionaryName).subscribe({
889
+ next: (items) => {
890
+ this.documentTypeOptions.set((items || [])
891
+ .map((item) => ({
892
+ value: String(item.key || ''),
893
+ label: String(item.label || item.key || ''),
894
+ }))
895
+ .filter((item) => !!item.value));
896
+ },
897
+ error: () => {
898
+ this.documentTypeOptions.set([]);
899
+ },
900
+ });
901
+ }, ...(ngDevMode ? [{ debugName: "documentTypeDictionaryEffect" }] : /* istanbul ignore next */ []));
902
+ openUploadModal() {
903
+ this.isUploadOpen.set(true);
904
+ }
905
+ closeUploadModal() {
906
+ this.isUploadOpen.set(false);
907
+ }
908
+ openTextModal(document) {
909
+ this.textTarget.set(document);
910
+ this.isTextOpen.set(true);
911
+ }
912
+ closeTextModal() {
913
+ this.isTextOpen.set(false);
914
+ this.textTarget.set(null);
915
+ }
916
+ openImagePreview(document) {
917
+ if (!this.isImage(document)) {
918
+ return;
919
+ }
920
+ this.imagePreviewTarget.set(document);
921
+ this.isImagePreviewOpen.set(true);
922
+ }
923
+ closeImagePreview() {
924
+ this.isImagePreviewOpen.set(false);
925
+ this.imagePreviewTarget.set(null);
926
+ }
927
+ recognizeText(document) {
928
+ if (!this.isImage(document)) {
929
+ return;
930
+ }
931
+ if (document.recognitionStatus === 'COMPLETED') {
932
+ return;
933
+ }
934
+ if (this.isDocumentRecognizing(document)) {
935
+ return;
936
+ }
937
+ this.recognizingDocumentId.set(document.id);
938
+ this.documentsService.recognize(document.id).subscribe({
939
+ next: (updated) => {
940
+ this.recognizingDocumentId.set(null);
941
+ this.upsertDocument(updated);
942
+ this.updateRecognitionPolling();
943
+ this.toast.show('Розпізнавання запущено', 'success', 2200);
944
+ if (this.textTarget()?.id === updated.id) {
945
+ this.textTarget.set(updated);
946
+ }
947
+ },
948
+ error: (error) => {
949
+ this.recognizingDocumentId.set(null);
950
+ this.toast.show(error?.error?.message || 'Не вдалося розпізнати дані', 'error', 3200);
951
+ },
952
+ });
953
+ }
954
+ removeDocument(document) {
955
+ if (!this.isAdmin() || this.deletingDocumentId()) {
956
+ return;
957
+ }
958
+ this.confirmService.open({
959
+ title: 'Видалити документ',
960
+ text: `Видалити документ «${document.name || 'Без назви'}»? Дію не можна буде скасувати.`,
961
+ submit: 'Видалити',
962
+ danger: true,
963
+ }).subscribe((confirmed) => {
964
+ if (!confirmed)
965
+ return;
966
+ this.deletingDocumentId.set(document.id);
967
+ this.documentsService.remove(document.id).subscribe({
968
+ next: () => {
969
+ this.documents.update((documents) => documents.filter((item) => item.id !== document.id));
970
+ if (this.textTarget()?.id === document.id)
971
+ this.closeTextModal();
972
+ if (this.imagePreviewTarget()?.id === document.id)
973
+ this.closeImagePreview();
974
+ this.deletingDocumentId.set(null);
975
+ this.updateRecognitionPolling();
976
+ this.toast.show('Документ видалено', 'success', 2200);
977
+ },
978
+ error: (error) => {
979
+ this.deletingDocumentId.set(null);
980
+ this.toast.show(error?.error?.message || 'Не вдалося видалити документ', 'error', 3200);
981
+ },
982
+ });
983
+ });
984
+ }
985
+ isDocumentRecognizing(document) {
986
+ return !!document?.id && this.recognizingDocumentId() === document.id;
987
+ }
988
+ trackDocument(_, document) {
989
+ return document.id;
990
+ }
991
+ getImagePreviewUrl(document) {
992
+ const match = String(document.downloadUrl || '').match(/^(.*\/api\/files\/[^/?#]+)(?:\?.*)?$/i);
993
+ return match ? `${match[1]}/preview?scale=25` : document.downloadUrl;
994
+ }
995
+ upsertDocument(updated) {
996
+ this.documents.update((current) => {
997
+ const next = current.slice();
998
+ const index = next.findIndex((item) => item.id === updated.id);
999
+ if (index >= 0) {
1000
+ next[index] = { ...next[index], ...updated };
1001
+ }
1002
+ return next;
1003
+ });
1004
+ }
1005
+ loadDocuments() {
1006
+ const entity = this.resolveEntity();
1007
+ const id = this.resolveId();
1008
+ if (!entity || !id) {
1009
+ this.documents.set([]);
1010
+ return;
1011
+ }
1012
+ this.isLoading.set(true);
1013
+ this.documentsService.list(entity, id).subscribe({
1014
+ next: (rows) => {
1015
+ this.documents.set(rows || []);
1016
+ this.isLoading.set(false);
1017
+ this.updateRecognitionPolling();
1018
+ },
1019
+ error: () => {
1020
+ this.documents.set([]);
1021
+ this.isLoading.set(false);
1022
+ },
1023
+ });
1024
+ }
1025
+ formatSize(size) {
1026
+ const normalizedSize = Number(size);
1027
+ if (!Number.isFinite(normalizedSize))
1028
+ return '-';
1029
+ size = normalizedSize;
1030
+ const bytes = Number(size || 0);
1031
+ if (!Number.isFinite(bytes) || bytes <= 0)
1032
+ return '0 B';
1033
+ const units = ['B', 'KB', 'MB', 'GB'];
1034
+ let value = bytes;
1035
+ let index = 0;
1036
+ while (value >= 1024 && index < units.length - 1) {
1037
+ value /= 1024;
1038
+ index += 1;
1039
+ }
1040
+ return `${value >= 10 || index === 0 ? Math.round(value) : value.toFixed(1)} ${units[index]}`;
1041
+ }
1042
+ resolveEntity() {
1043
+ return String(this.entity() || '').trim();
1044
+ }
1045
+ resolveId() {
1046
+ return String(this.id() || '').trim();
1047
+ }
1048
+ isImage(document) {
1049
+ return String(document.type || '').startsWith('image/');
1050
+ }
1051
+ isPdf(document) {
1052
+ const type = String(document.type || '').toLowerCase();
1053
+ const name = String(document.name || '').toLowerCase();
1054
+ return type === 'application/pdf' || name.endsWith('.pdf');
1055
+ }
1056
+ getFileBadge(document) {
1057
+ if (this.isPdf(document))
1058
+ return 'PDF';
1059
+ if (this.isImage(document))
1060
+ return 'IMG';
1061
+ const name = String(document.name || '').trim();
1062
+ if (name.includes('.')) {
1063
+ return name.split('.').pop().slice(0, 5).toUpperCase();
1064
+ }
1065
+ return 'FILE';
1066
+ }
1067
+ getDocumentTypeLabel(document) {
1068
+ const label = String(document.documentTypeLabel || '').trim();
1069
+ if (label)
1070
+ return label;
1071
+ const documentTypeId = String(document.documentTypeId || '').trim();
1072
+ if (!documentTypeId)
1073
+ return '';
1074
+ return this.documentTypeOptions().find((option) => option.value === documentTypeId)?.label ?? documentTypeId;
1075
+ }
1076
+ reloadDocuments() {
1077
+ this.loadDocuments();
1078
+ }
1079
+ isRecognitionPending(document) {
1080
+ return this.isImage(document) && (document.recognitionStatus === 'PENDING' || document.recognitionStatus === 'PROCESSING');
1081
+ }
1082
+ isRecognitionCompleted(document) {
1083
+ return this.isImage(document) && document.recognitionStatus === 'COMPLETED';
1084
+ }
1085
+ getRecognitionStatusLabel(document) {
1086
+ if (document.recognitionStatus === 'FAILED')
1087
+ return 'Не вдалося розпізнати';
1088
+ if (this.isRecognitionPending(document)) {
1089
+ return document.recognitionStatus === 'PROCESSING' ? 'Розпізнається у фоні' : 'Очікує розпізнавання';
1090
+ }
1091
+ return document.recognitionStatus === 'COMPLETED' ? 'Розпізнано' : '';
1092
+ }
1093
+ updateRecognitionPolling() {
1094
+ if (!this.documents().some((document) => this.isRecognitionPending(document))) {
1095
+ this.recognitionPolling?.unsubscribe();
1096
+ this.recognitionPolling = undefined;
1097
+ return;
1098
+ }
1099
+ if (this.recognitionPolling)
1100
+ return;
1101
+ this.recognitionPolling = timer(2500, 4000)
1102
+ .pipe(exhaustMap(() => {
1103
+ const pendingDocuments = this.documents().filter((document) => this.isRecognitionPending(document));
1104
+ if (!pendingDocuments.length)
1105
+ return of([]);
1106
+ return forkJoin(pendingDocuments.map((document) => this.documentsService.recognitionStatus(document.id).pipe(catchError(() => of(null))))).pipe(map((statuses) => statuses.filter((status) => !!status)));
1107
+ }), map((statuses) => {
1108
+ statuses.forEach((status) => this.upsertDocument(status));
1109
+ return statuses;
1110
+ }), takeWhile(() => this.documents().some((document) => this.isRecognitionPending(document)), true), takeUntilDestroyed(this.destroyRef), takeUntil(timer(RECOGNITION_POLLING_TIMEOUT_MS)), finalize(() => {
1111
+ this.recognitionPolling = undefined;
1112
+ }))
1113
+ .subscribe();
1114
+ }
1115
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DocumentsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1116
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: DocumentsComponent, isStandalone: true, selector: "app-documents", inputs: { entity: { classPropertyName: "entity", publicName: "entity", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"space-y-4\">\n <div>\n @if (isLoading()) {\n <div class=\"rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-8 text-center text-sm text-slate-500\">\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0456\u0432...\n </div>\n } @else if (documents().length === 0) {\n <div class=\"rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-8 text-center text-sm text-slate-500\">\n \u0414\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0438 \u0432\u0456\u0434\u0441\u0443\u0442\u043D\u0456\n </div>\n } @else {\n <div class=\"grid gap-3\">\n @for (document of documents(); track trackDocument($index, document)) {\n <article class=\"flex flex-col gap-4 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between\">\n @if (isImage(document)) {\n <button\n type=\"button\"\n class=\"block shrink-0 overflow-hidden rounded-2xl border border-slate-200 bg-slate-50 transition hover:border-slate-300 hover:bg-slate-100 h-28 w-40\"\n (click)=\"openImagePreview(document)\"\n >\n <img [src]=\"getImagePreviewUrl(document)\" [alt]=\"document.name || '\u0414\u043E\u043A\u0443\u043C\u0435\u043D\u0442'\" loading=\"lazy\" decoding=\"async\" class=\"h-full w-full object-cover\">\n </button>\n } @else {\n <a\n class=\"block shrink-0 overflow-hidden rounded-2xl border border-slate-200 bg-slate-50 transition hover:border-slate-300 hover:bg-slate-100\"\n [href]=\"document.downloadUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n [class.h-28]=\"isPdf(document)\"\n [class.w-40]=\"isPdf(document)\"\n [class.h-20]=\"!isPdf(document)\"\n [class.w-24]=\"!isPdf(document)\"\n >\n @if (isPdf(document)) {\n <div class=\"flex h-full w-full flex-col items-center justify-center gap-2 px-3 py-4 text-slate-700\">\n <div class=\"flex h-11 w-11 items-center justify-center rounded-2xl bg-white text-slate-900 shadow-sm\">\n <lucide-icon [img]=\"fileIcon\"></lucide-icon>\n </div>\n <span class=\"text-xs font-semibold uppercase tracking-[0.18em] text-slate-600\">PDF</span>\n </div>\n } @else {\n <div class=\"flex h-full w-full flex-col items-center justify-center gap-2 px-3 py-4 text-slate-700\">\n <div class=\"flex h-11 w-11 items-center justify-center rounded-2xl bg-white text-slate-900 shadow-sm\">\n <lucide-icon [img]=\"fileIcon\"></lucide-icon>\n </div>\n <span class=\"text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500\">\n {{ getFileBadge(document) }}\n </span>\n </div>\n }\n </a>\n }\n\n <div class=\"min-w-0 flex-1\">\n <div class=\"flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between\">\n <div class=\"min-w-0\">\n <div class=\"flex flex-wrap items-center gap-2\">\n <h4 class=\"truncate text-sm font-semibold text-slate-900\">\n {{ document.name || '\u0411\u0435\u0437 \u043D\u0430\u0437\u0432\u0438' }}\n </h4>\n @if (getDocumentTypeLabel(document)) {\n <span class=\"inline-flex shrink-0 items-center rounded-full border border-sky-200 bg-sky-50 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-700\">\n {{ getDocumentTypeLabel(document) }}\n </span>\n }\n </div>\n <p class=\"mt-1 text-xs text-slate-500\">\n {{ formatSize(document.size) }} \u00B7 {{ document.type }}\n </p>\n </div>\n\n <div class=\"flex flex-wrap items-center gap-2\">\n <ui-button variant=\"ghost\" size=\"sm\" (click)=\"openTextModal(document)\">\n <lucide-icon [img]=\"eyeIcon\"></lucide-icon>\n \u041F\u0435\u0440\u0435\u0433\u043B\u044F\u0434\n </ui-button>\n @if (isImage(document)) {\n <ui-button variant=\"ghost\" size=\"sm\" [disabled]=\"isDocumentRecognizing(document) || isRecognitionPending(document) || isRecognitionCompleted(document)\" (click)=\"recognizeText(document)\">\n <lucide-icon [img]=\"sparkleIcon\"></lucide-icon>\n {{ isDocumentRecognizing(document) ? '\u0420\u043E\u0437\u043F\u0456\u0437\u043D\u0430\u0432\u0430\u043D\u043D\u044F...' : '\u0420\u043E\u0437\u043F\u0456\u0437\u043D\u0430\u0442\u0438 \u0434\u0430\u043D\u0456' }}\n </ui-button>\n }\n @if (isAdmin()) {\n <ui-button variant=\"ghost\" size=\"sm\" [disabled]=\"deletingDocumentId() === document.id\" (click)=\"removeDocument(document)\">\n <lucide-icon [img]=\"deleteIcon\"></lucide-icon>\n {{ deletingDocumentId() === document.id ? '\u0412\u0438\u0434\u0430\u043B\u0435\u043D\u043D\u044F...' : '\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438' }}\n </ui-button>\n }\n </div>\n </div>\n\n <div class=\"mt-3 flex flex-wrap items-center justify-between gap-2\">\n <a\n class=\"inline-flex items-center rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm font-medium text-slate-700 transition hover:border-slate-300 hover:bg-slate-50\"\n [href]=\"document.downloadUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438\n </a>\n @if (isImage(document) && getRecognitionStatusLabel(document)) {\n <span\n class=\"inline-flex items-center rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.12em]\"\n [class.border-amber-200]=\"isRecognitionPending(document)\"\n [class.bg-amber-50]=\"isRecognitionPending(document)\"\n [class.text-amber-700]=\"isRecognitionPending(document)\"\n [class.border-rose-200]=\"document.recognitionStatus === 'FAILED'\"\n [class.bg-rose-50]=\"document.recognitionStatus === 'FAILED'\"\n [class.text-rose-700]=\"document.recognitionStatus === 'FAILED'\"\n [class.border-emerald-200]=\"document.recognitionStatus === 'COMPLETED'\"\n [class.bg-emerald-50]=\"document.recognitionStatus === 'COMPLETED'\"\n [class.text-emerald-700]=\"document.recognitionStatus === 'COMPLETED'\"\n >\n {{ getRecognitionStatusLabel(document) }}\n </span>\n }\n </div>\n </div>\n </article>\n }\n </div>\n }\n </div>\n\n <div class=\"flex flex-col gap-3 rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-4\">\n <div>\n <h4 class=\"text-sm font-semibold text-slate-900\">\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0456\u0432 \u0443 \u0444\u0430\u0439\u043B\u043E\u0432\u0435 \u0441\u0445\u043E\u0432\u0438\u0449\u0435</h4>\n <p class=\"mt-1 text-xs text-slate-500\">\u0414\u043E\u0434\u0430\u0439\u0442\u0435 \u043D\u043E\u0432\u0456 \u0444\u0430\u0439\u043B\u0438 \u0434\u043E \u043A\u0430\u0440\u0442\u043A\u0438 \u0431\u0435\u0437\u043F\u043E\u0441\u0435\u0440\u0435\u0434\u043D\u044C\u043E \u0442\u0443\u0442.</p>\n </div>\n\n <div class=\"flex w-full flex-col gap-2 sm:w-auto\">\n <ui-button variant=\"secondary\" size=\"sm\" (click)=\"openUploadModal()\">\n <lucide-icon [img]=\"plusIcon\"></lucide-icon>\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\n </ui-button>\n </div>\n </div>\n</div>\n\n<app-documents-text-modal\n [isOpen]=\"isTextOpen()\"\n [document]=\"textTarget()\"\n (closed)=\"closeTextModal()\"\n (recognizeRequested)=\"textTarget() && recognizeText(textTarget()!)\"\n></app-documents-text-modal>\n\n<app-documents-upload-modal\n [isOpen]=\"isUploadOpen()\"\n [entity]=\"entity()\"\n [id]=\"id()\"\n (closed)=\"closeUploadModal()\"\n (saved)=\"reloadDocuments()\"\n></app-documents-upload-modal>\n\n<app-documents-image-preview-modal\n [isOpen]=\"isImagePreviewOpen()\"\n [document]=\"imagePreviewTarget()\"\n (closed)=\"closeImagePreview()\"\n></app-documents-image-preview-modal>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1$2.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: DocumentsUploadModalComponent, selector: "app-documents-upload-modal", inputs: ["isOpen", "entity", "id"], outputs: ["closed", "saved"] }, { kind: "component", type: DocumentsTextModalComponent, selector: "app-documents-text-modal", inputs: ["isOpen", "document"], outputs: ["closed", "recognizeRequested"] }, { kind: "component", type: DocumentsImagePreviewModalComponent, selector: "app-documents-image-preview-modal", inputs: ["isOpen", "document"], outputs: ["closed"] }] });
1117
+ }
1118
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DocumentsComponent, decorators: [{
1119
+ type: Component,
1120
+ args: [{ selector: 'app-documents', standalone: true, imports: [
1121
+ CommonModule,
1122
+ LucideAngularModule,
1123
+ UiButtonComponent,
1124
+ DocumentsUploadModalComponent,
1125
+ DocumentsTextModalComponent,
1126
+ DocumentsImagePreviewModalComponent,
1127
+ ], template: "<div class=\"space-y-4\">\n <div>\n @if (isLoading()) {\n <div class=\"rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-8 text-center text-sm text-slate-500\">\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0456\u0432...\n </div>\n } @else if (documents().length === 0) {\n <div class=\"rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-8 text-center text-sm text-slate-500\">\n \u0414\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0438 \u0432\u0456\u0434\u0441\u0443\u0442\u043D\u0456\n </div>\n } @else {\n <div class=\"grid gap-3\">\n @for (document of documents(); track trackDocument($index, document)) {\n <article class=\"flex flex-col gap-4 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between\">\n @if (isImage(document)) {\n <button\n type=\"button\"\n class=\"block shrink-0 overflow-hidden rounded-2xl border border-slate-200 bg-slate-50 transition hover:border-slate-300 hover:bg-slate-100 h-28 w-40\"\n (click)=\"openImagePreview(document)\"\n >\n <img [src]=\"getImagePreviewUrl(document)\" [alt]=\"document.name || '\u0414\u043E\u043A\u0443\u043C\u0435\u043D\u0442'\" loading=\"lazy\" decoding=\"async\" class=\"h-full w-full object-cover\">\n </button>\n } @else {\n <a\n class=\"block shrink-0 overflow-hidden rounded-2xl border border-slate-200 bg-slate-50 transition hover:border-slate-300 hover:bg-slate-100\"\n [href]=\"document.downloadUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n [class.h-28]=\"isPdf(document)\"\n [class.w-40]=\"isPdf(document)\"\n [class.h-20]=\"!isPdf(document)\"\n [class.w-24]=\"!isPdf(document)\"\n >\n @if (isPdf(document)) {\n <div class=\"flex h-full w-full flex-col items-center justify-center gap-2 px-3 py-4 text-slate-700\">\n <div class=\"flex h-11 w-11 items-center justify-center rounded-2xl bg-white text-slate-900 shadow-sm\">\n <lucide-icon [img]=\"fileIcon\"></lucide-icon>\n </div>\n <span class=\"text-xs font-semibold uppercase tracking-[0.18em] text-slate-600\">PDF</span>\n </div>\n } @else {\n <div class=\"flex h-full w-full flex-col items-center justify-center gap-2 px-3 py-4 text-slate-700\">\n <div class=\"flex h-11 w-11 items-center justify-center rounded-2xl bg-white text-slate-900 shadow-sm\">\n <lucide-icon [img]=\"fileIcon\"></lucide-icon>\n </div>\n <span class=\"text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500\">\n {{ getFileBadge(document) }}\n </span>\n </div>\n }\n </a>\n }\n\n <div class=\"min-w-0 flex-1\">\n <div class=\"flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between\">\n <div class=\"min-w-0\">\n <div class=\"flex flex-wrap items-center gap-2\">\n <h4 class=\"truncate text-sm font-semibold text-slate-900\">\n {{ document.name || '\u0411\u0435\u0437 \u043D\u0430\u0437\u0432\u0438' }}\n </h4>\n @if (getDocumentTypeLabel(document)) {\n <span class=\"inline-flex shrink-0 items-center rounded-full border border-sky-200 bg-sky-50 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-700\">\n {{ getDocumentTypeLabel(document) }}\n </span>\n }\n </div>\n <p class=\"mt-1 text-xs text-slate-500\">\n {{ formatSize(document.size) }} \u00B7 {{ document.type }}\n </p>\n </div>\n\n <div class=\"flex flex-wrap items-center gap-2\">\n <ui-button variant=\"ghost\" size=\"sm\" (click)=\"openTextModal(document)\">\n <lucide-icon [img]=\"eyeIcon\"></lucide-icon>\n \u041F\u0435\u0440\u0435\u0433\u043B\u044F\u0434\n </ui-button>\n @if (isImage(document)) {\n <ui-button variant=\"ghost\" size=\"sm\" [disabled]=\"isDocumentRecognizing(document) || isRecognitionPending(document) || isRecognitionCompleted(document)\" (click)=\"recognizeText(document)\">\n <lucide-icon [img]=\"sparkleIcon\"></lucide-icon>\n {{ isDocumentRecognizing(document) ? '\u0420\u043E\u0437\u043F\u0456\u0437\u043D\u0430\u0432\u0430\u043D\u043D\u044F...' : '\u0420\u043E\u0437\u043F\u0456\u0437\u043D\u0430\u0442\u0438 \u0434\u0430\u043D\u0456' }}\n </ui-button>\n }\n @if (isAdmin()) {\n <ui-button variant=\"ghost\" size=\"sm\" [disabled]=\"deletingDocumentId() === document.id\" (click)=\"removeDocument(document)\">\n <lucide-icon [img]=\"deleteIcon\"></lucide-icon>\n {{ deletingDocumentId() === document.id ? '\u0412\u0438\u0434\u0430\u043B\u0435\u043D\u043D\u044F...' : '\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438' }}\n </ui-button>\n }\n </div>\n </div>\n\n <div class=\"mt-3 flex flex-wrap items-center justify-between gap-2\">\n <a\n class=\"inline-flex items-center rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm font-medium text-slate-700 transition hover:border-slate-300 hover:bg-slate-50\"\n [href]=\"document.downloadUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438\n </a>\n @if (isImage(document) && getRecognitionStatusLabel(document)) {\n <span\n class=\"inline-flex items-center rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.12em]\"\n [class.border-amber-200]=\"isRecognitionPending(document)\"\n [class.bg-amber-50]=\"isRecognitionPending(document)\"\n [class.text-amber-700]=\"isRecognitionPending(document)\"\n [class.border-rose-200]=\"document.recognitionStatus === 'FAILED'\"\n [class.bg-rose-50]=\"document.recognitionStatus === 'FAILED'\"\n [class.text-rose-700]=\"document.recognitionStatus === 'FAILED'\"\n [class.border-emerald-200]=\"document.recognitionStatus === 'COMPLETED'\"\n [class.bg-emerald-50]=\"document.recognitionStatus === 'COMPLETED'\"\n [class.text-emerald-700]=\"document.recognitionStatus === 'COMPLETED'\"\n >\n {{ getRecognitionStatusLabel(document) }}\n </span>\n }\n </div>\n </div>\n </article>\n }\n </div>\n }\n </div>\n\n <div class=\"flex flex-col gap-3 rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-4\">\n <div>\n <h4 class=\"text-sm font-semibold text-slate-900\">\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0456\u0432 \u0443 \u0444\u0430\u0439\u043B\u043E\u0432\u0435 \u0441\u0445\u043E\u0432\u0438\u0449\u0435</h4>\n <p class=\"mt-1 text-xs text-slate-500\">\u0414\u043E\u0434\u0430\u0439\u0442\u0435 \u043D\u043E\u0432\u0456 \u0444\u0430\u0439\u043B\u0438 \u0434\u043E \u043A\u0430\u0440\u0442\u043A\u0438 \u0431\u0435\u0437\u043F\u043E\u0441\u0435\u0440\u0435\u0434\u043D\u044C\u043E \u0442\u0443\u0442.</p>\n </div>\n\n <div class=\"flex w-full flex-col gap-2 sm:w-auto\">\n <ui-button variant=\"secondary\" size=\"sm\" (click)=\"openUploadModal()\">\n <lucide-icon [img]=\"plusIcon\"></lucide-icon>\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\n </ui-button>\n </div>\n </div>\n</div>\n\n<app-documents-text-modal\n [isOpen]=\"isTextOpen()\"\n [document]=\"textTarget()\"\n (closed)=\"closeTextModal()\"\n (recognizeRequested)=\"textTarget() && recognizeText(textTarget()!)\"\n></app-documents-text-modal>\n\n<app-documents-upload-modal\n [isOpen]=\"isUploadOpen()\"\n [entity]=\"entity()\"\n [id]=\"id()\"\n (closed)=\"closeUploadModal()\"\n (saved)=\"reloadDocuments()\"\n></app-documents-upload-modal>\n\n<app-documents-image-preview-modal\n [isOpen]=\"isImagePreviewOpen()\"\n [document]=\"imagePreviewTarget()\"\n (closed)=\"closeImagePreview()\"\n></app-documents-image-preview-modal>\n" }]
1128
+ }], propDecorators: { entity: [{ type: i0.Input, args: [{ isSignal: true, alias: "entity", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }] } });
1129
+
1130
+ class HistoryTabComponent {
1131
+ entity = input('', ...(ngDevMode ? [{ debugName: "entity" }] : /* istanbul ignore next */ []));
1132
+ primaryEntity = input('', ...(ngDevMode ? [{ debugName: "primaryEntity" }] : /* istanbul ignore next */ []));
1133
+ id = input('', ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
1134
+ entityLabels = input({}, ...(ngDevMode ? [{ debugName: "entityLabels" }] : /* istanbul ignore next */ []));
1135
+ operations = signal([], ...(ngDevMode ? [{ debugName: "operations" }] : /* istanbul ignore next */ []));
1136
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
1137
+ hasError = signal(false, ...(ngDevMode ? [{ debugName: "hasError" }] : /* istanbul ignore next */ []));
1138
+ expandedOperationIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedOperationIds" }] : /* istanbul ignore next */ []));
1139
+ viewerUrls = signal([], ...(ngDevMode ? [{ debugName: "viewerUrls" }] : /* istanbul ignore next */ []));
1140
+ isViewerOpen = signal(false, ...(ngDevMode ? [{ debugName: "isViewerOpen" }] : /* istanbul ignore next */ []));
1141
+ http = inject(HttpClient);
1142
+ imageService = inject(CardImageService);
1143
+ baseUrl = inject(UI_CARD_API_BASE_URL);
1144
+ eventBus = inject(EventBus);
1145
+ destroyRef = inject(DestroyRef);
1146
+ refreshSequence = signal(0, ...(ngDevMode ? [{ debugName: "refreshSequence" }] : /* istanbul ignore next */ []));
1147
+ lastRequestKey = '';
1148
+ constructor() {
1149
+ this.eventBus.on(ENTITY_UPDATED)
1150
+ .pipe(takeUntilDestroyed(this.destroyRef))
1151
+ .subscribe((event) => {
1152
+ if (event.entity && event.entity !== this.entity())
1153
+ return;
1154
+ if (event.recordId && event.recordId !== this.id())
1155
+ return;
1156
+ this.refreshSequence.update((value) => value + 1);
1157
+ });
1158
+ // Document events contain the useful information in the related entity
1159
+ // change. Keep those details visible without requiring an extra click.
1160
+ effect(() => {
1161
+ const documentOperationIds = new Set(this.operations()
1162
+ .filter((operation) => operation.actionCode.startsWith('documents.'))
1163
+ .map((operation) => operation.id));
1164
+ if (!documentOperationIds.size)
1165
+ return;
1166
+ const next = new Set(untracked(() => this.expandedOperationIds()));
1167
+ documentOperationIds.forEach((id) => next.add(id));
1168
+ this.expandedOperationIds.set(next);
1169
+ });
1170
+ }
1171
+ loadEffect = effect(() => {
1172
+ const entity = this.entity().trim();
1173
+ const id = this.id().trim();
1174
+ const refreshSequence = this.refreshSequence();
1175
+ const requestKey = `${entity}:${id}:${refreshSequence}`;
1176
+ if (requestKey === this.lastRequestKey)
1177
+ return;
1178
+ this.lastRequestKey = requestKey;
1179
+ if (!entity || !id) {
1180
+ this.operations.set([]);
1181
+ return;
1182
+ }
1183
+ this.isLoading.set(true);
1184
+ this.hasError.set(false);
1185
+ const cacheBuster = Date.now();
1186
+ this.http
1187
+ .get(cardApiResourceUrl(this.baseUrl, `/api/crud/${encodeURIComponent(entity)}/${encodeURIComponent(id)}/history?_ts=${cacheBuster}`), {
1188
+ headers: new HttpHeaders({
1189
+ 'Cache-Control': 'no-cache, no-store, max-age=0',
1190
+ Pragma: 'no-cache',
1191
+ }),
1192
+ })
1193
+ .pipe(takeUntilDestroyed(this.destroyRef))
1194
+ .subscribe({
1195
+ next: (operations) => {
1196
+ this.operations.set(operations || []);
1197
+ this.isLoading.set(false);
1198
+ },
1199
+ error: () => {
1200
+ this.operations.set([]);
1201
+ this.hasError.set(true);
1202
+ this.isLoading.set(false);
1203
+ },
1204
+ });
1205
+ }, ...(ngDevMode ? [{ debugName: "loadEffect" }] : /* istanbul ignore next */ []));
1206
+ toggleOperation(id) {
1207
+ const next = new Set(this.expandedOperationIds());
1208
+ if (next.has(id))
1209
+ next.delete(id);
1210
+ else
1211
+ next.add(id);
1212
+ this.expandedOperationIds.set(next);
1213
+ }
1214
+ isExpanded(id) {
1215
+ return this.expandedOperationIds().has(id);
1216
+ }
1217
+ getEntityLabel(entityType) {
1218
+ const labels = {
1219
+ entity_files: 'Документ',
1220
+ files: 'Файл',
1221
+ };
1222
+ return this.entityLabels()[entityType] || labels[entityType] || entityType;
1223
+ }
1224
+ getActionLabel(action) {
1225
+ const labels = {
1226
+ created: 'Створено',
1227
+ updated: 'Змінено',
1228
+ deleted: 'Видалено',
1229
+ };
1230
+ return labels[action] || action;
1231
+ }
1232
+ isImageField(field) {
1233
+ return field.type === 'image';
1234
+ }
1235
+ isHtmlField(field) {
1236
+ return field.type === 'html';
1237
+ }
1238
+ isFileField(field) {
1239
+ return field.type === 'file';
1240
+ }
1241
+ getImageUrls(value) {
1242
+ const values = Array.isArray(value) ? value : [value];
1243
+ return values
1244
+ .map((item) => String(item ?? '').trim())
1245
+ .filter(Boolean)
1246
+ .map((id) => this.imageService.getPhotoLink(id) || '')
1247
+ .filter(Boolean);
1248
+ }
1249
+ openImageViewer(urls) {
1250
+ if (!urls.length)
1251
+ return;
1252
+ this.viewerUrls.set(urls);
1253
+ this.isViewerOpen.set(true);
1254
+ }
1255
+ closeImageViewer() {
1256
+ this.isViewerOpen.set(false);
1257
+ }
1258
+ getAffectedSummary(operation) {
1259
+ const counts = new Map();
1260
+ for (const change of this.getRelatedChanges(operation)) {
1261
+ counts.set(change.entityType, (counts.get(change.entityType) || 0) + 1);
1262
+ }
1263
+ return [...counts.entries()]
1264
+ .map(([type, count]) => `${this.getEntityLabel(type)}: ${count}`)
1265
+ .join(' · ');
1266
+ }
1267
+ getPrimaryChanges(operation) {
1268
+ const primaryTypes = new Set([this.entity(), this.primaryEntity()].filter(Boolean));
1269
+ return (operation.changes || []).filter((change) => primaryTypes.has(change.entityType));
1270
+ }
1271
+ getRelatedChanges(operation) {
1272
+ const primaryChanges = this.getPrimaryChanges(operation);
1273
+ const primaryTypes = new Set([this.entity(), this.primaryEntity()].filter(Boolean));
1274
+ return primaryChanges.length
1275
+ ? (operation.changes || []).filter((change) => !primaryTypes.has(change.entityType))
1276
+ : operation.changes || [];
1277
+ }
1278
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: HistoryTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1279
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: HistoryTabComponent, isStandalone: true, selector: "app-history-tab", inputs: { entity: { classPropertyName: "entity", publicName: "entity", isSignal: true, isRequired: false, transformFunction: null }, primaryEntity: { classPropertyName: "primaryEntity", publicName: "primaryEntity", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, entityLabels: { classPropertyName: "entityLabels", publicName: "entityLabels", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<section class=\"space-y-4\">\n @if (isLoading()) {\n <div class=\"rounded-2xl border border-slate-200 bg-white p-6 text-sm text-slate-500\">\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F \u0456\u0441\u0442\u043E\u0440\u0456\u0457\u2026\n </div>\n } @else if (hasError()) {\n <div class=\"rounded-2xl border border-red-200 bg-red-50 p-6 text-sm text-red-700\">\n \u041D\u0435 \u0432\u0434\u0430\u043B\u043E\u0441\u044F \u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0456\u0441\u0442\u043E\u0440\u0456\u044E \u0437\u043C\u0456\u043D.\n </div>\n } @else if (!operations().length) {\n <div class=\"rounded-2xl border border-slate-200 bg-white p-6 text-sm text-slate-500\">\n \u0406\u0441\u0442\u043E\u0440\u0456\u044F \u0437\u043C\u0456\u043D \u043F\u043E\u043A\u0438 \u043F\u043E\u0440\u043E\u0436\u043D\u044F.\n </div>\n } @else {\n <div class=\"space-y-3\">\n @for (operation of operations(); track operation.id) {\n <article class=\"rounded-2xl border border-slate-200 bg-white p-5 shadow-sm\">\n <div class=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h3 class=\"text-sm font-semibold text-slate-900\">{{ operation.description }}</h3>\n @if (operation.actorName) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ operation.actorName }}</p>\n }\n </div>\n <time class=\"text-xs text-slate-400\" [dateTime]=\"operation.createdAt\">\n {{ operation.createdAt | date: 'dd.MM.yyyy HH:mm' }}\n </time>\n </div>\n\n @if (getPrimaryChanges(operation).length) {\n <div class=\"mt-4 space-y-2 border-t border-slate-100 pt-4\">\n @for (change of getPrimaryChanges(operation); track change.entityId + change.entityType) {\n @for (field of (change.changedFields | keyvalue); track field.key) {\n <div class=\"flex flex-wrap items-center gap-2 text-sm\">\n <span class=\"font-medium text-slate-700\">{{ field.value.label }}</span>\n @if (isImageField(field.value)) {\n @if (getImageUrls(field.value.from.value).length) {\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (url of getImageUrls(field.value.from.value); track url; let imageIndex = $index) {\n <button type=\"button\" class=\"h-14 w-14 overflow-hidden rounded-lg border border-slate-200 bg-slate-100\" (click)=\"openImageViewer(getImageUrls(field.value.from.value))\" [attr.aria-label]=\"'\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u0441\u0442\u0430\u0440\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\">\n <img [src]=\"url\" [alt]=\"'\u0421\u0442\u0430\u0440\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\" class=\"h-full w-full object-cover\" />\n </button>\n }\n </div>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isFileField(field.value)) {\n @if (field.value.from.url) {\n <a class=\"font-medium text-sky-700 underline\" [href]=\"field.value.from.url\" target=\"_blank\" rel=\"noopener noreferrer\">\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0444\u0430\u0439\u043B</a>\n } @else {\n <span class=\"text-slate-400\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isHtmlField(field.value)) {\n @if (field.value.from.display !== '\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E') {\n <div class=\"prose prose-sm max-w-none text-slate-500\" [innerHTML]=\"field.value.from.display\"></div>\n } @else {\n <span class=\"text-slate-400\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else {\n <span class=\"font-medium text-slate-900\">{{ field.value.from.display }}</span>\n }\n <span class=\"text-slate-400\">\u2192</span>\n @if (isImageField(field.value)) {\n @if (getImageUrls(field.value.to.value).length) {\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (url of getImageUrls(field.value.to.value); track url; let imageIndex = $index) {\n <button type=\"button\" class=\"h-14 w-14 overflow-hidden rounded-lg border border-slate-200 bg-slate-100\" (click)=\"openImageViewer(getImageUrls(field.value.to.value))\" [attr.aria-label]=\"'\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043D\u043E\u0432\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\">\n <img [src]=\"url\" [alt]=\"'\u041D\u043E\u0432\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\" class=\"h-full w-full object-cover\" />\n </button>\n }\n </div>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isFileField(field.value)) {\n @if (field.value.to.url) {\n <a class=\"font-medium text-sky-700 underline\" [href]=\"field.value.to.url\" target=\"_blank\" rel=\"noopener noreferrer\">\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0444\u0430\u0439\u043B</a>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isHtmlField(field.value)) {\n @if (field.value.to.display !== '\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E') {\n <div class=\"prose prose-sm max-w-none text-slate-900\" [innerHTML]=\"field.value.to.display\"></div>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else {\n <span class=\"font-medium text-slate-900\">{{ field.value.to.display }}</span>\n }\n </div>\n }\n }\n </div>\n }\n\n @if (getRelatedChanges(operation).length) {\n <div class=\"mt-4 border-t border-slate-100 pt-4\">\n <button\n type=\"button\"\n class=\"flex w-full items-center justify-between gap-3 text-left text-sm text-slate-600\"\n [attr.aria-expanded]=\"isExpanded(operation.id)\"\n (click)=\"toggleOperation(operation.id)\"\n >\n <span>\n \u0414\u0435\u0442\u0430\u043B\u0456 \u0437\u043C\u0456\u043D:\n <span class=\"font-medium text-slate-900\">{{ getAffectedSummary(operation) }}</span>\n </span>\n <span class=\"text-lg leading-none text-slate-400\">{{ isExpanded(operation.id) ? '\u2212' : '+' }}</span>\n </button>\n\n @if (isExpanded(operation.id)) {\n <div class=\"mt-4 space-y-4\">\n @for (change of getRelatedChanges(operation); track change.entityId + change.entityType) {\n <div class=\"rounded-xl bg-slate-50 p-3\">\n <p class=\"mb-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-500\">\n {{ getEntityLabel(change.entityType) }} \u00B7 {{ getActionLabel(change.action) }}\n </p>\n @for (field of (change.changedFields | keyvalue); track field.key) {\n <div class=\"flex flex-wrap items-center gap-2 text-sm\">\n <span class=\"font-medium text-slate-700\">{{ field.value.label }}</span>\n @if (isImageField(field.value)) {\n @if (getImageUrls(field.value.from.value).length) {\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (url of getImageUrls(field.value.from.value); track url; let imageIndex = $index) {\n <button type=\"button\" class=\"h-14 w-14 overflow-hidden rounded-lg border border-slate-200 bg-white\" (click)=\"openImageViewer(getImageUrls(field.value.from.value))\" [attr.aria-label]=\"'\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u0441\u0442\u0430\u0440\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\">\n <img [src]=\"url\" [alt]=\"'\u0421\u0442\u0430\u0440\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\" class=\"h-full w-full object-cover\" />\n </button>\n }\n </div>\n } @else {\n <span class=\"text-slate-400\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isFileField(field.value)) {\n @if (field.value.from.url) {\n <a class=\"text-sky-700 underline\" [href]=\"field.value.from.url\" target=\"_blank\" rel=\"noopener noreferrer\">\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0444\u0430\u0439\u043B</a>\n } @else {\n <span class=\"text-slate-400\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isHtmlField(field.value)) {\n @if (field.value.from.display !== '\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E') {\n <div class=\"prose prose-sm max-w-none text-slate-500\" [innerHTML]=\"field.value.from.display\"></div>\n } @else {\n <span class=\"text-slate-400\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else {\n <span class=\"text-slate-400\">{{ field.value.from.display }}</span>\n }\n <span class=\"text-slate-400\">\u2192</span>\n @if (isImageField(field.value)) {\n @if (getImageUrls(field.value.to.value).length) {\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (url of getImageUrls(field.value.to.value); track url; let imageIndex = $index) {\n <button type=\"button\" class=\"h-14 w-14 overflow-hidden rounded-lg border border-slate-200 bg-white\" (click)=\"openImageViewer(getImageUrls(field.value.to.value))\" [attr.aria-label]=\"'\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043D\u043E\u0432\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\">\n <img [src]=\"url\" [alt]=\"'\u041D\u043E\u0432\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\" class=\"h-full w-full object-cover\" />\n </button>\n }\n </div>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isFileField(field.value)) {\n @if (field.value.to.url) {\n <a class=\"font-medium text-sky-700 underline\" [href]=\"field.value.to.url\" target=\"_blank\" rel=\"noopener noreferrer\">\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0444\u0430\u0439\u043B</a>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isHtmlField(field.value)) {\n @if (field.value.to.display !== '\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E') {\n <div class=\"prose prose-sm max-w-none text-slate-900\" [innerHTML]=\"field.value.to.display\"></div>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else {\n <span class=\"font-medium text-slate-900\">{{ field.value.to.display }}</span>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </article>\n }\n </div>\n }\n</section>\n\n@if (isViewerOpen()) {\n <app-images-viewer [urls]=\"viewerUrls()\" (closed)=\"closeImageViewer()\" />\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: ImagesViewerComponent, selector: "app-images-viewer", inputs: ["urls", "initialIndex"], outputs: ["closed"] }, { kind: "pipe", type: i1.DatePipe, name: "date" }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
1280
+ }
1281
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: HistoryTabComponent, decorators: [{
1282
+ type: Component,
1283
+ args: [{ selector: 'app-history-tab', standalone: true, imports: [CommonModule, ImagesViewerComponent], template: "<section class=\"space-y-4\">\n @if (isLoading()) {\n <div class=\"rounded-2xl border border-slate-200 bg-white p-6 text-sm text-slate-500\">\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F \u0456\u0441\u0442\u043E\u0440\u0456\u0457\u2026\n </div>\n } @else if (hasError()) {\n <div class=\"rounded-2xl border border-red-200 bg-red-50 p-6 text-sm text-red-700\">\n \u041D\u0435 \u0432\u0434\u0430\u043B\u043E\u0441\u044F \u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0456\u0441\u0442\u043E\u0440\u0456\u044E \u0437\u043C\u0456\u043D.\n </div>\n } @else if (!operations().length) {\n <div class=\"rounded-2xl border border-slate-200 bg-white p-6 text-sm text-slate-500\">\n \u0406\u0441\u0442\u043E\u0440\u0456\u044F \u0437\u043C\u0456\u043D \u043F\u043E\u043A\u0438 \u043F\u043E\u0440\u043E\u0436\u043D\u044F.\n </div>\n } @else {\n <div class=\"space-y-3\">\n @for (operation of operations(); track operation.id) {\n <article class=\"rounded-2xl border border-slate-200 bg-white p-5 shadow-sm\">\n <div class=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h3 class=\"text-sm font-semibold text-slate-900\">{{ operation.description }}</h3>\n @if (operation.actorName) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ operation.actorName }}</p>\n }\n </div>\n <time class=\"text-xs text-slate-400\" [dateTime]=\"operation.createdAt\">\n {{ operation.createdAt | date: 'dd.MM.yyyy HH:mm' }}\n </time>\n </div>\n\n @if (getPrimaryChanges(operation).length) {\n <div class=\"mt-4 space-y-2 border-t border-slate-100 pt-4\">\n @for (change of getPrimaryChanges(operation); track change.entityId + change.entityType) {\n @for (field of (change.changedFields | keyvalue); track field.key) {\n <div class=\"flex flex-wrap items-center gap-2 text-sm\">\n <span class=\"font-medium text-slate-700\">{{ field.value.label }}</span>\n @if (isImageField(field.value)) {\n @if (getImageUrls(field.value.from.value).length) {\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (url of getImageUrls(field.value.from.value); track url; let imageIndex = $index) {\n <button type=\"button\" class=\"h-14 w-14 overflow-hidden rounded-lg border border-slate-200 bg-slate-100\" (click)=\"openImageViewer(getImageUrls(field.value.from.value))\" [attr.aria-label]=\"'\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u0441\u0442\u0430\u0440\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\">\n <img [src]=\"url\" [alt]=\"'\u0421\u0442\u0430\u0440\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\" class=\"h-full w-full object-cover\" />\n </button>\n }\n </div>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isFileField(field.value)) {\n @if (field.value.from.url) {\n <a class=\"font-medium text-sky-700 underline\" [href]=\"field.value.from.url\" target=\"_blank\" rel=\"noopener noreferrer\">\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0444\u0430\u0439\u043B</a>\n } @else {\n <span class=\"text-slate-400\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isHtmlField(field.value)) {\n @if (field.value.from.display !== '\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E') {\n <div class=\"prose prose-sm max-w-none text-slate-500\" [innerHTML]=\"field.value.from.display\"></div>\n } @else {\n <span class=\"text-slate-400\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else {\n <span class=\"font-medium text-slate-900\">{{ field.value.from.display }}</span>\n }\n <span class=\"text-slate-400\">\u2192</span>\n @if (isImageField(field.value)) {\n @if (getImageUrls(field.value.to.value).length) {\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (url of getImageUrls(field.value.to.value); track url; let imageIndex = $index) {\n <button type=\"button\" class=\"h-14 w-14 overflow-hidden rounded-lg border border-slate-200 bg-slate-100\" (click)=\"openImageViewer(getImageUrls(field.value.to.value))\" [attr.aria-label]=\"'\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043D\u043E\u0432\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\">\n <img [src]=\"url\" [alt]=\"'\u041D\u043E\u0432\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\" class=\"h-full w-full object-cover\" />\n </button>\n }\n </div>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isFileField(field.value)) {\n @if (field.value.to.url) {\n <a class=\"font-medium text-sky-700 underline\" [href]=\"field.value.to.url\" target=\"_blank\" rel=\"noopener noreferrer\">\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0444\u0430\u0439\u043B</a>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isHtmlField(field.value)) {\n @if (field.value.to.display !== '\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E') {\n <div class=\"prose prose-sm max-w-none text-slate-900\" [innerHTML]=\"field.value.to.display\"></div>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else {\n <span class=\"font-medium text-slate-900\">{{ field.value.to.display }}</span>\n }\n </div>\n }\n }\n </div>\n }\n\n @if (getRelatedChanges(operation).length) {\n <div class=\"mt-4 border-t border-slate-100 pt-4\">\n <button\n type=\"button\"\n class=\"flex w-full items-center justify-between gap-3 text-left text-sm text-slate-600\"\n [attr.aria-expanded]=\"isExpanded(operation.id)\"\n (click)=\"toggleOperation(operation.id)\"\n >\n <span>\n \u0414\u0435\u0442\u0430\u043B\u0456 \u0437\u043C\u0456\u043D:\n <span class=\"font-medium text-slate-900\">{{ getAffectedSummary(operation) }}</span>\n </span>\n <span class=\"text-lg leading-none text-slate-400\">{{ isExpanded(operation.id) ? '\u2212' : '+' }}</span>\n </button>\n\n @if (isExpanded(operation.id)) {\n <div class=\"mt-4 space-y-4\">\n @for (change of getRelatedChanges(operation); track change.entityId + change.entityType) {\n <div class=\"rounded-xl bg-slate-50 p-3\">\n <p class=\"mb-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-500\">\n {{ getEntityLabel(change.entityType) }} \u00B7 {{ getActionLabel(change.action) }}\n </p>\n @for (field of (change.changedFields | keyvalue); track field.key) {\n <div class=\"flex flex-wrap items-center gap-2 text-sm\">\n <span class=\"font-medium text-slate-700\">{{ field.value.label }}</span>\n @if (isImageField(field.value)) {\n @if (getImageUrls(field.value.from.value).length) {\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (url of getImageUrls(field.value.from.value); track url; let imageIndex = $index) {\n <button type=\"button\" class=\"h-14 w-14 overflow-hidden rounded-lg border border-slate-200 bg-white\" (click)=\"openImageViewer(getImageUrls(field.value.from.value))\" [attr.aria-label]=\"'\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u0441\u0442\u0430\u0440\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\">\n <img [src]=\"url\" [alt]=\"'\u0421\u0442\u0430\u0440\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\" class=\"h-full w-full object-cover\" />\n </button>\n }\n </div>\n } @else {\n <span class=\"text-slate-400\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isFileField(field.value)) {\n @if (field.value.from.url) {\n <a class=\"text-sky-700 underline\" [href]=\"field.value.from.url\" target=\"_blank\" rel=\"noopener noreferrer\">\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0444\u0430\u0439\u043B</a>\n } @else {\n <span class=\"text-slate-400\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isHtmlField(field.value)) {\n @if (field.value.from.display !== '\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E') {\n <div class=\"prose prose-sm max-w-none text-slate-500\" [innerHTML]=\"field.value.from.display\"></div>\n } @else {\n <span class=\"text-slate-400\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else {\n <span class=\"text-slate-400\">{{ field.value.from.display }}</span>\n }\n <span class=\"text-slate-400\">\u2192</span>\n @if (isImageField(field.value)) {\n @if (getImageUrls(field.value.to.value).length) {\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (url of getImageUrls(field.value.to.value); track url; let imageIndex = $index) {\n <button type=\"button\" class=\"h-14 w-14 overflow-hidden rounded-lg border border-slate-200 bg-white\" (click)=\"openImageViewer(getImageUrls(field.value.to.value))\" [attr.aria-label]=\"'\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043D\u043E\u0432\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\">\n <img [src]=\"url\" [alt]=\"'\u041D\u043E\u0432\u0435 \u0444\u043E\u0442\u043E ' + (imageIndex + 1)\" class=\"h-full w-full object-cover\" />\n </button>\n }\n </div>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isFileField(field.value)) {\n @if (field.value.to.url) {\n <a class=\"font-medium text-sky-700 underline\" [href]=\"field.value.to.url\" target=\"_blank\" rel=\"noopener noreferrer\">\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0444\u0430\u0439\u043B</a>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else if (isHtmlField(field.value)) {\n @if (field.value.to.display !== '\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E') {\n <div class=\"prose prose-sm max-w-none text-slate-900\" [innerHTML]=\"field.value.to.display\"></div>\n } @else {\n <span class=\"font-medium text-slate-900\">\u041D\u0435 \u0437\u0430\u0434\u0430\u043D\u043E</span>\n }\n } @else {\n <span class=\"font-medium text-slate-900\">{{ field.value.to.display }}</span>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </article>\n }\n </div>\n }\n</section>\n\n@if (isViewerOpen()) {\n <app-images-viewer [urls]=\"viewerUrls()\" (closed)=\"closeImageViewer()\" />\n}\n" }]
1284
+ }], ctorParameters: () => [], propDecorators: { entity: [{ type: i0.Input, args: [{ isSignal: true, alias: "entity", required: false }] }], primaryEntity: [{ type: i0.Input, args: [{ isSignal: true, alias: "primaryEntity", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], entityLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityLabels", required: false }] }] } });
1285
+
1286
+ const ACTION_API = 'ACTION_API';
1287
+ const ACTION_FORM = 'ACTION_FORM';
1288
+ const ACTION_PRINT = 'ACTION_PRINT';
1289
+ const normalizeActionType = (value) => {
1290
+ const raw = String(value || '').trim().toLowerCase();
1291
+ if (raw === 'api' || raw === ACTION_API.toLowerCase())
1292
+ return ACTION_API;
1293
+ if (raw === 'form' || raw === ACTION_FORM.toLowerCase())
1294
+ return ACTION_FORM;
1295
+ if (raw === 'print' || raw === ACTION_PRINT.toLowerCase())
1296
+ return ACTION_PRINT;
1297
+ return raw ? raw.toUpperCase() : null;
1298
+ };
1299
+ class CardTableDataService {
1300
+ http = inject(HttpClient);
1301
+ baseUrl = inject(UI_CARD_API_BASE_URL);
1302
+ getTableView(code, options) {
1303
+ let params = new HttpParams();
1304
+ for (const [key, value] of Object.entries(options)) {
1305
+ if (value !== undefined && value !== null && value !== '')
1306
+ params = params.set(key, typeof value === 'object' ? JSON.stringify(value) : String(value));
1307
+ }
1308
+ return this.http.get(cardApiResourceUrl(this.baseUrl, `/api/crud/${encodeURIComponent(code)}`), { params }).pipe(map$1((response) => ({
1309
+ ...(response.config || response),
1310
+ rows: response.rows ?? response.items ?? [],
1311
+ pagination: {
1312
+ page: response.page,
1313
+ size: response.size,
1314
+ totalElements: response.total,
1315
+ totalPages: response.totalPages,
1316
+ },
1317
+ })));
1318
+ }
1319
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTableDataService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1320
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTableDataService, providedIn: 'root' });
1321
+ }
1322
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTableDataService, decorators: [{
1323
+ type: Injectable,
1324
+ args: [{ providedIn: 'root' }]
1325
+ }] });
1326
+ class CardTableWarehouseService {
1327
+ http = inject(HttpClient);
1328
+ baseUrl = inject(UI_CARD_API_BASE_URL);
1329
+ getWarehouses() {
1330
+ return this.http.get(cardApiResourceUrl(this.baseUrl, '/api/warehouses')).pipe(map$1((items) => ({ items: items || [] })));
1331
+ }
1332
+ getWarehouseItems(id) {
1333
+ return this.http.get(cardApiResourceUrl(this.baseUrl, `/api/warehouses/${encodeURIComponent(id)}/items`));
1334
+ }
1335
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTableWarehouseService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1336
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTableWarehouseService, providedIn: 'root' });
1337
+ }
1338
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTableWarehouseService, decorators: [{
1339
+ type: Injectable,
1340
+ args: [{ providedIn: 'root' }]
1341
+ }] });
1342
+ class CardTableBarcodeService {
1343
+ printBarcode(options) {
1344
+ const barcode = String(options.barcode || '').trim();
1345
+ if (!barcode || typeof window === 'undefined')
1346
+ return;
1347
+ const printWindow = window.open('', '_blank', 'width=860,height=920');
1348
+ if (!printWindow)
1349
+ return;
1350
+ const barcodeSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1351
+ try {
1352
+ JsBarcode(barcodeSvg, barcode, { format: 'CODE128', displayValue: false, margin: 0, width: 2.1, height: 88, lineColor: '#0f172a', background: '#ffffff' });
1353
+ }
1354
+ catch {
1355
+ printWindow.close();
1356
+ return;
1357
+ }
1358
+ printWindow.document.write(`<!doctype html><html><head><title>${this.escape(options.title)}</title><style>body{font-family:Arial;text-align:center;padding:10mm}.barcode{width:58mm;margin:auto}.barcode svg{width:100%;height:18mm}</style></head><body><h2>${this.escape(options.title)}</h2><div class="barcode">${barcodeSvg.outerHTML}</div></body></html>`);
1359
+ printWindow.document.close();
1360
+ printWindow.focus();
1361
+ printWindow.print();
1362
+ }
1363
+ escape(value) { return value.replace(/[&<>'"]/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[char] || char)); }
1364
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTableBarcodeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1365
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTableBarcodeService, providedIn: 'root' });
1366
+ }
1367
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTableBarcodeService, decorators: [{
1368
+ type: Injectable,
1369
+ args: [{ providedIn: 'root' }]
1370
+ }] });
1371
+ class CardTablePrintService {
1372
+ openPrintWindow() { return typeof window === 'undefined' ? null : window.open('', '_blank', 'width=860,height=920'); }
1373
+ print(config) {
1374
+ const printWindow = this.openPrintWindow();
1375
+ return printWindow ? this.printInWindow(printWindow, config) : false;
1376
+ }
1377
+ printInWindow(printWindow, config) {
1378
+ printWindow.document.write(`<!doctype html><html><head><title>${config.title || 'Друк'}</title></head><body>${config.html}</body></html>`);
1379
+ printWindow.document.close();
1380
+ printWindow.focus();
1381
+ printWindow.print();
1382
+ return true;
1383
+ }
1384
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTablePrintService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1385
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTablePrintService, providedIn: 'root' });
1386
+ }
1387
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTablePrintService, decorators: [{
1388
+ type: Injectable,
1389
+ args: [{ providedIn: 'root' }]
1390
+ }] });
1391
+
1392
+ const getConditionPathValue = (source, pathRaw) => {
1393
+ const path = String(pathRaw || '').trim();
1394
+ if (!path)
1395
+ return undefined;
1396
+ const parts = path.split('.');
1397
+ let current = source;
1398
+ for (const part of parts) {
1399
+ if (current == null || typeof current !== 'object')
1400
+ return undefined;
1401
+ current = current[part];
1402
+ }
1403
+ return current;
1404
+ };
1405
+ const evaluateCondition = (conditionRaw, context) => {
1406
+ const condition = String(conditionRaw || '').trim();
1407
+ if (!condition)
1408
+ return true;
1409
+ const tokens = [];
1410
+ let index = 0;
1411
+ const pushOperator = (value) => {
1412
+ tokens.push({ type: 'operator', value });
1413
+ };
1414
+ while (index < condition.length) {
1415
+ const char = condition[index];
1416
+ if (/\s/.test(char)) {
1417
+ index += 1;
1418
+ continue;
1419
+ }
1420
+ const nextThree = condition.slice(index, index + 3);
1421
+ const nextTwo = condition.slice(index, index + 2);
1422
+ if (nextThree === '===') {
1423
+ pushOperator('===');
1424
+ index += 3;
1425
+ continue;
1426
+ }
1427
+ if (nextThree === '!==') {
1428
+ pushOperator('!==');
1429
+ index += 3;
1430
+ continue;
1431
+ }
1432
+ if (nextTwo === '==') {
1433
+ pushOperator('==');
1434
+ index += 2;
1435
+ continue;
1436
+ }
1437
+ if (nextTwo === '!=') {
1438
+ pushOperator('!=');
1439
+ index += 2;
1440
+ continue;
1441
+ }
1442
+ if (nextTwo === '&&') {
1443
+ pushOperator('&&');
1444
+ index += 2;
1445
+ continue;
1446
+ }
1447
+ if (nextTwo === '||') {
1448
+ pushOperator('||');
1449
+ index += 2;
1450
+ continue;
1451
+ }
1452
+ if (char === '!' || char === '(' || char === ')') {
1453
+ pushOperator(char);
1454
+ index += 1;
1455
+ continue;
1456
+ }
1457
+ if (char === '"' || char === "'") {
1458
+ const quote = char;
1459
+ index += 1;
1460
+ let value = '';
1461
+ let escaped = false;
1462
+ let closed = false;
1463
+ while (index < condition.length) {
1464
+ const current = condition[index];
1465
+ index += 1;
1466
+ if (escaped) {
1467
+ value += current;
1468
+ escaped = false;
1469
+ continue;
1470
+ }
1471
+ if (current === '\\') {
1472
+ escaped = true;
1473
+ continue;
1474
+ }
1475
+ if (current === quote) {
1476
+ closed = true;
1477
+ break;
1478
+ }
1479
+ value += current;
1480
+ }
1481
+ if (!closed)
1482
+ return true;
1483
+ tokens.push({ type: 'string', value });
1484
+ continue;
1485
+ }
1486
+ if (/[0-9.-]/.test(char)) {
1487
+ const match = condition.slice(index).match(/^-?\d+(?:\.\d+)?/);
1488
+ if (match) {
1489
+ tokens.push({ type: 'number', value: Number(match[0]) });
1490
+ index += match[0].length;
1491
+ continue;
1492
+ }
1493
+ }
1494
+ if (/[A-Za-z_$]/.test(char)) {
1495
+ const match = condition.slice(index).match(/^[A-Za-z_$][A-Za-z0-9_.$-]*/);
1496
+ if (match) {
1497
+ const raw = match[0];
1498
+ const normalized = raw.toLowerCase();
1499
+ if (normalized === 'true' || normalized === 'false') {
1500
+ tokens.push({ type: 'boolean', value: normalized === 'true' });
1501
+ }
1502
+ else if (normalized === 'null') {
1503
+ tokens.push({ type: 'null', value: null });
1504
+ }
1505
+ else {
1506
+ tokens.push({ type: 'identifier', value: raw });
1507
+ }
1508
+ index += raw.length;
1509
+ continue;
1510
+ }
1511
+ }
1512
+ return true;
1513
+ }
1514
+ let tokenIndex = 0;
1515
+ const peek = () => tokens[tokenIndex];
1516
+ const consume = () => {
1517
+ const token = tokens[tokenIndex];
1518
+ tokenIndex += 1;
1519
+ return token;
1520
+ };
1521
+ const matchOperator = (...values) => {
1522
+ const token = peek();
1523
+ if (token?.type !== 'operator')
1524
+ return false;
1525
+ if (!values.includes(token.value))
1526
+ return false;
1527
+ tokenIndex += 1;
1528
+ return true;
1529
+ };
1530
+ const toBoolean = (value) => {
1531
+ if (typeof value === 'string')
1532
+ return value.trim().length > 0;
1533
+ return Boolean(value);
1534
+ };
1535
+ const getValue = (identifierRaw) => {
1536
+ const identifier = String(identifierRaw || '').trim();
1537
+ if (!identifier)
1538
+ return undefined;
1539
+ if (identifier === 'data')
1540
+ return context;
1541
+ if (identifier.startsWith('data.'))
1542
+ return getConditionPathValue(context, identifier.slice(5));
1543
+ return getConditionPathValue(context, identifier);
1544
+ };
1545
+ const parseExpression = () => parseOr();
1546
+ const parseOr = () => {
1547
+ let left = parseAnd();
1548
+ while (matchOperator('||')) {
1549
+ const right = parseAnd();
1550
+ left = toBoolean(left) || toBoolean(right);
1551
+ }
1552
+ return left;
1553
+ };
1554
+ const parseAnd = () => {
1555
+ let left = parseEquality();
1556
+ while (matchOperator('&&')) {
1557
+ const right = parseEquality();
1558
+ left = toBoolean(left) && toBoolean(right);
1559
+ }
1560
+ return left;
1561
+ };
1562
+ const parseEquality = () => {
1563
+ let left = parseUnary();
1564
+ while (true) {
1565
+ if (matchOperator('==')) {
1566
+ left = left == parseUnary();
1567
+ continue;
1568
+ }
1569
+ if (matchOperator('!=')) {
1570
+ left = left != parseUnary();
1571
+ continue;
1572
+ }
1573
+ if (matchOperator('===')) {
1574
+ left = left === parseUnary();
1575
+ continue;
1576
+ }
1577
+ if (matchOperator('!==')) {
1578
+ left = left !== parseUnary();
1579
+ continue;
1580
+ }
1581
+ break;
1582
+ }
1583
+ return left;
1584
+ };
1585
+ const parseUnary = () => {
1586
+ if (matchOperator('!')) {
1587
+ return !toBoolean(parseUnary());
1588
+ }
1589
+ return parsePrimary();
1590
+ };
1591
+ const parsePrimary = () => {
1592
+ const token = consume();
1593
+ if (!token)
1594
+ return undefined;
1595
+ if (token.type === 'operator' && token.value === '(') {
1596
+ const value = parseExpression();
1597
+ matchOperator(')');
1598
+ return value;
1599
+ }
1600
+ if (token.type === 'identifier')
1601
+ return getValue(token.value);
1602
+ return token.value;
1603
+ };
1604
+ return toBoolean(parseExpression());
1605
+ };
1606
+ class CardTableComponent {
1607
+ tab;
1608
+ data = null;
1609
+ tableViewsService = inject(CardTableDataService);
1610
+ warehouseService = inject(CardTableWarehouseService);
1611
+ barcodePrintService = inject(CardTableBarcodeService);
1612
+ eventBus = inject(EventBus);
1613
+ host = inject(UI_ENTITY_CARD_HOST, { optional: true });
1614
+ actionBus = inject(CardTablePrintService);
1615
+ tableColumnsSignal = signal([], ...(ngDevMode ? [{ debugName: "tableColumnsSignal" }] : /* istanbul ignore next */ []));
1616
+ tableRowsSignal = signal([], ...(ngDevMode ? [{ debugName: "tableRowsSignal" }] : /* istanbul ignore next */ []));
1617
+ tablePaginationSignal = signal({ page: 0, pageSize: 10, total: 0 }, ...(ngDevMode ? [{ debugName: "tablePaginationSignal" }] : /* istanbul ignore next */ []));
1618
+ tableHeaderActionsSignal = signal([], ...(ngDevMode ? [{ debugName: "tableHeaderActionsSignal" }] : /* istanbul ignore next */ []));
1619
+ editableColumnOptionsSignal = signal({}, ...(ngDevMode ? [{ debugName: "editableColumnOptionsSignal" }] : /* istanbul ignore next */ []));
1620
+ isTableLoadingSignal = signal(false, ...(ngDevMode ? [{ debugName: "isTableLoadingSignal" }] : /* istanbul ignore next */ []));
1621
+ isTableActionRunningSignal = signal(false, ...(ngDevMode ? [{ debugName: "isTableActionRunningSignal" }] : /* istanbul ignore next */ []));
1622
+ activeEditableCellSignal = signal(null, ...(ngDevMode ? [{ debugName: "activeEditableCellSignal" }] : /* istanbul ignore next */ []));
1623
+ activeFormTargetSignal = signal(null, ...(ngDevMode ? [{ debugName: "activeFormTargetSignal" }] : /* istanbul ignore next */ []));
1624
+ activeFormTarget = computed(() => this.activeFormTargetSignal(), ...(ngDevMode ? [{ debugName: "activeFormTarget" }] : /* istanbul ignore next */ []));
1625
+ tableData = computed(() => ({
1626
+ items: this.tableRowsSignal(),
1627
+ pagination: this.tablePaginationSignal(),
1628
+ }), ...(ngDevMode ? [{ debugName: "tableData" }] : /* istanbul ignore next */ []));
1629
+ pageIndex = 0;
1630
+ pageSize = 10;
1631
+ sortField = '';
1632
+ sortDirection = '';
1633
+ tableViewColumns = [];
1634
+ editableColumnsLoaded = false;
1635
+ ngOnChanges() {
1636
+ this.pageIndex = 0;
1637
+ this.pageSize = this.resolvePageSize();
1638
+ this.sortField = '';
1639
+ this.sortDirection = '';
1640
+ this.editableColumnsLoaded = false;
1641
+ this.activeEditableCellSignal.set(null);
1642
+ this.editableColumnOptionsSignal.set({});
1643
+ this.loadTable();
1644
+ }
1645
+ get disablePaging() {
1646
+ const value = this.tab?.props?.['disablePaging'];
1647
+ return value === true || value === 'true';
1648
+ }
1649
+ get tableColumns() {
1650
+ return this.tableColumnsSignal();
1651
+ }
1652
+ get tableHeaderActions() {
1653
+ return this.tableHeaderActionsSignal();
1654
+ }
1655
+ get tableActions() {
1656
+ const blockActions = Array.isArray(this.tab?.actions) ? this.tab.actions : [];
1657
+ return [
1658
+ ...blockActions.map((action, index) => {
1659
+ const rawAction = action;
1660
+ return {
1661
+ ...action,
1662
+ variant: rawAction.variant || (index === 0 ? 'primary' : 'ghost'),
1663
+ };
1664
+ }),
1665
+ ...this.tableHeaderActions.map((action) => ({
1666
+ ...action,
1667
+ variant: 'ghost',
1668
+ })),
1669
+ ];
1670
+ }
1671
+ get isTableLoading() {
1672
+ return this.isTableLoadingSignal();
1673
+ }
1674
+ get isTableActionRunning() {
1675
+ return this.isTableActionRunningSignal();
1676
+ }
1677
+ get rowClickable() {
1678
+ return this.isEditEnabled();
1679
+ }
1680
+ onTablePageChange(event) {
1681
+ this.pageIndex = Math.max(0, Number(event['pageIndex'] ?? this.pageIndex) || 0);
1682
+ this.pageSize = Math.max(1, Number(event['pageSize'] ?? this.pageSize) || this.pageSize);
1683
+ const active = String(event['active'] ?? '').trim();
1684
+ this.sortField = this.tableViewColumns.find((column) => column.id === active)?.id || '';
1685
+ const direction = String(event['direction'] ?? '').toLowerCase();
1686
+ this.sortDirection = direction === 'asc' || direction === 'desc' ? direction : '';
1687
+ this.loadTable();
1688
+ }
1689
+ onTableAction(event) {
1690
+ if (normalizeActionType(event.action.type) === ACTION_PRINT || event.action.component === 'warehouseBarcodePrint' || event.action.template || event.action.id === 'print-certificate') {
1691
+ this.runPrintAction(event.action, event.row);
1692
+ return;
1693
+ }
1694
+ if (normalizeActionType(event.action.type) === ACTION_FORM && String(event.action.formId || '').trim()) {
1695
+ this.runFormAction(event.action, event.row);
1696
+ return;
1697
+ }
1698
+ if (!event.action.api) {
1699
+ return;
1700
+ }
1701
+ const payload = this.resolveTemplatePayload(event.action.payload || {}, event.row);
1702
+ this.runApiAction({
1703
+ api: this.resolveTemplateString(event.action.api, {
1704
+ ...event.row,
1705
+ ...payload,
1706
+ }),
1707
+ method: event.action.method || 'POST',
1708
+ payload,
1709
+ title: event.action.title,
1710
+ text: event.action.text,
1711
+ submit: event.action.submit,
1712
+ danger: event.action.danger,
1713
+ successMessage: event.action.id === 'issue'
1714
+ ? 'Матеріали списано зі складу'
1715
+ : event.action.id === 'delete'
1716
+ ? 'Позицію видалено'
1717
+ : 'Дію виконано',
1718
+ errorMessage: 'Не вдалося виконати дію',
1719
+ });
1720
+ }
1721
+ onHeaderAction(action) {
1722
+ if (!action?.formId) {
1723
+ if (action?.api) {
1724
+ this.runApiAction({
1725
+ api: action.api,
1726
+ method: action.method || 'POST',
1727
+ payload: this.resolveTemplatePayload(action.payload || {}, this.data || {}),
1728
+ title: action.title,
1729
+ text: action.text,
1730
+ submit: action.submit,
1731
+ danger: action.variant === 'danger',
1732
+ successMessage: 'Дію виконано',
1733
+ errorMessage: 'Не вдалося виконати дію',
1734
+ });
1735
+ }
1736
+ return;
1737
+ }
1738
+ this.runHeaderFormAction(action);
1739
+ }
1740
+ onTableRowClick(row) {
1741
+ if (!this.isEditEnabled()) {
1742
+ return;
1743
+ }
1744
+ this.runRowEditAction(row);
1745
+ }
1746
+ onTableCellChange(event) {
1747
+ const editableConfig = this.resolveEditableColumnsConfig().find((column) => column.key === event.column.key);
1748
+ if (!editableConfig) {
1749
+ return;
1750
+ }
1751
+ const rowId = String(event.row['id'] ?? '').trim();
1752
+ if (!rowId) {
1753
+ return;
1754
+ }
1755
+ const nextValue = String(event.value ?? '').trim();
1756
+ const nextRow = {
1757
+ ...event.row,
1758
+ [event.column.bindKey || event.column.key]: nextValue,
1759
+ };
1760
+ if (editableConfig.editor === 'select') {
1761
+ nextRow[event.column.key] =
1762
+ this.resolveEditableColumnOptions(event.column.key).find((option) => String(option.value ?? '').trim() === nextValue)?.label ?? '';
1763
+ }
1764
+ this.tableRowsSignal.update((rows) => rows.map((row) => (String(row['id'] ?? '').trim() === rowId ? nextRow : row)));
1765
+ if (editableConfig.api) {
1766
+ this.saveEditableColumnChange(editableConfig, nextRow);
1767
+ }
1768
+ }
1769
+ onEditableCellActivate(row, columnKey, event) {
1770
+ event.stopPropagation();
1771
+ const rowId = String(row['id'] ?? '').trim();
1772
+ const key = String(columnKey || '').trim();
1773
+ if (!rowId || !key) {
1774
+ return;
1775
+ }
1776
+ this.activeEditableCellSignal.set({ rowId, columnKey: key });
1777
+ }
1778
+ isEditableCellActive(row, columnKey) {
1779
+ const active = this.activeEditableCellSignal();
1780
+ return !!active && active.rowId === String(row['id'] ?? '').trim() && active.columnKey === String(columnKey || '').trim();
1781
+ }
1782
+ resolveEditableDisplayValue(row, column) {
1783
+ const rawValue = this.getCellValue(row, column.bindKey || column.key);
1784
+ if (rawValue === null || rawValue === undefined || rawValue === '') {
1785
+ return '—';
1786
+ }
1787
+ if (column.editor === 'select') {
1788
+ const value = String(rawValue).trim();
1789
+ const option = this.resolveEditableColumnOptions(column.key).find((item) => String(item.value ?? '').trim() === value);
1790
+ return String(option?.label || rawValue).trim() || '—';
1791
+ }
1792
+ return String(rawValue);
1793
+ }
1794
+ getCellValue(row, column) {
1795
+ return row?.[column];
1796
+ }
1797
+ runPrintAction(action, row) {
1798
+ const template = String(action.template || (action.id === 'print-certificate' ? 'print-certificate' : '')).trim();
1799
+ const recordId = String(row['id'] || '').trim();
1800
+ if (template && recordId) {
1801
+ const printWindow = this.actionBus.openPrintWindow();
1802
+ const entity = encodeURIComponent(this.resolveTableSource());
1803
+ const id = encodeURIComponent(recordId);
1804
+ const actionId = encodeURIComponent(template);
1805
+ this.host?.callApi({
1806
+ api: `/api/crud/${entity}/${id}/action/${actionId}`,
1807
+ method: 'GET',
1808
+ }).subscribe({
1809
+ next: (response) => {
1810
+ const html = String(response?.data?.html || '').trim();
1811
+ const title = String(response?.data?.title || action.label || 'Друк');
1812
+ if (!html) {
1813
+ printWindow?.close();
1814
+ this.notify('Шаблон друку порожній', 'error');
1815
+ return;
1816
+ }
1817
+ const config = { html, title };
1818
+ if (printWindow)
1819
+ this.actionBus.printInWindow(printWindow, config);
1820
+ else
1821
+ this.actionBus.print(config);
1822
+ },
1823
+ error: (error) => {
1824
+ printWindow?.close();
1825
+ this.notify(error?.error?.message || 'Не вдалося завантажити сертифікат', 'error');
1826
+ },
1827
+ });
1828
+ return;
1829
+ }
1830
+ const payload = this.resolveTemplatePayload(action.payload || {}, row);
1831
+ const barcode = String(payload['barcode'] ?? row['barcode'] ?? '').trim();
1832
+ if (!barcode) {
1833
+ this.notify('У рядку немає штрихкоду', 'warning');
1834
+ return;
1835
+ }
1836
+ const title = String(payload['title'] ?? row['name'] ?? this.tab?.title ?? 'Штрихкод').trim() || 'Штрихкод';
1837
+ this.barcodePrintService.printBarcode({ barcode, title });
1838
+ }
1839
+ closeFormAction() {
1840
+ this.activeFormTargetSignal.set(null);
1841
+ }
1842
+ refreshTable() {
1843
+ this.loadTable();
1844
+ }
1845
+ loadTable() {
1846
+ const table = this.resolveTableSource();
1847
+ this.isTableLoadingSignal.set(true);
1848
+ this.tableRowsSignal.set([]);
1849
+ this.tablePaginationSignal.set({ page: this.pageIndex, pageSize: this.pageSize, total: 0 });
1850
+ if (!table) {
1851
+ this.tableColumnsSignal.set([]);
1852
+ this.isTableLoadingSignal.set(false);
1853
+ return;
1854
+ }
1855
+ this.tableViewsService.getTableView(table, {
1856
+ page: this.pageIndex,
1857
+ size: this.pageSize,
1858
+ sortField: this.sortField,
1859
+ sortDirection: this.sortDirection,
1860
+ filters: this.resolveTableFilters(),
1861
+ }).subscribe({
1862
+ next: (view) => {
1863
+ const rows = this.normalizeTableRows(view);
1864
+ this.tableViewColumns = view.columns || [];
1865
+ this.tableHeaderActionsSignal.set(view.actions?.header?.custom || []);
1866
+ this.tableColumnsSignal.set(this.normalizeTableColumns(this.tableViewColumns));
1867
+ this.tableRowsSignal.set(rows);
1868
+ this.activeEditableCellSignal.set(null);
1869
+ this.loadEditableColumnOptions();
1870
+ this.pageIndex = view.pagination?.page ?? this.pageIndex;
1871
+ this.pageSize = view.pagination?.size ?? this.pageSize;
1872
+ this.tablePaginationSignal.set({
1873
+ page: this.pageIndex,
1874
+ pageSize: this.pageSize,
1875
+ total: view.pagination?.totalElements ?? rows.length,
1876
+ });
1877
+ this.isTableLoadingSignal.set(false);
1878
+ },
1879
+ error: (error) => {
1880
+ this.tableRowsSignal.set([]);
1881
+ this.tableHeaderActionsSignal.set([]);
1882
+ this.tablePaginationSignal.set({ page: this.pageIndex, pageSize: this.pageSize, total: 0 });
1883
+ this.activeEditableCellSignal.set(null);
1884
+ this.isTableLoadingSignal.set(false);
1885
+ this.notify(error?.error?.message || 'Не вдалося завантажити таблицю', 'error');
1886
+ },
1887
+ });
1888
+ }
1889
+ runApiAction(config) {
1890
+ const api = String(config.api || '').trim();
1891
+ if (!api) {
1892
+ return;
1893
+ }
1894
+ this.isTableActionRunningSignal.set(true);
1895
+ this.host?.callApi({
1896
+ api,
1897
+ method: config.method || 'POST',
1898
+ title: config.title,
1899
+ text: config.text,
1900
+ submit: typeof config.submit === 'string' ? config.submit : undefined,
1901
+ danger: config.danger,
1902
+ payload: config.payload,
1903
+ }).subscribe({
1904
+ next: () => {
1905
+ this.notify(config.successMessage, 'success');
1906
+ this.isTableActionRunningSignal.set(false);
1907
+ this.loadTable();
1908
+ this.eventBus.emit(ENTITY_UPDATED, {
1909
+ source: 'card-table.row-action',
1910
+ });
1911
+ },
1912
+ error: (error) => {
1913
+ this.isTableActionRunningSignal.set(false);
1914
+ this.notify(error?.error?.message || config.errorMessage, 'error');
1915
+ },
1916
+ });
1917
+ }
1918
+ runFormAction(action, row) {
1919
+ const formId = String(action.formId || '').trim();
1920
+ const recordId = String(row['id'] || '').trim();
1921
+ if (!formId || !recordId) {
1922
+ this.notify('Не вдалося відкрити форму редагування', 'warning');
1923
+ return;
1924
+ }
1925
+ const actionPayload = this.resolveTemplatePayload(action.payload || {}, row);
1926
+ this.activeFormTargetSignal.set({
1927
+ formId,
1928
+ recordId,
1929
+ initialValue: { ...row, ...actionPayload },
1930
+ });
1931
+ }
1932
+ runRowEditAction(row) {
1933
+ const recordId = String(row['id'] || '').trim();
1934
+ const formId = this.resolveTableSource();
1935
+ if (!recordId || !formId) {
1936
+ return;
1937
+ }
1938
+ this.activeFormTargetSignal.set({
1939
+ formId,
1940
+ recordId,
1941
+ initialValue: { ...row },
1942
+ });
1943
+ }
1944
+ runHeaderFormAction(action) {
1945
+ const formId = String(action.formId || '').trim();
1946
+ if (!formId) {
1947
+ return;
1948
+ }
1949
+ this.activeFormTargetSignal.set({
1950
+ formId,
1951
+ recordId: '',
1952
+ initialValue: this.resolveTemplatePayload(action.payload || {}, this.data || {}),
1953
+ });
1954
+ }
1955
+ normalizeTableColumns(columns) {
1956
+ const editableColumns = this.resolveEditableColumnsConfig();
1957
+ return (columns || [])
1958
+ .filter((column) => !column.hidden)
1959
+ .map((column) => {
1960
+ const editableConfig = editableColumns.find((item) => item.key === column.id);
1961
+ if (!editableConfig) {
1962
+ return {
1963
+ key: column.id,
1964
+ labelKey: column.label,
1965
+ sortable: true,
1966
+ type: column.type,
1967
+ width: column.width,
1968
+ link: column.link,
1969
+ };
1970
+ }
1971
+ return {
1972
+ key: column.id,
1973
+ labelKey: column.label,
1974
+ sortable: true,
1975
+ type: column.type,
1976
+ width: column.width,
1977
+ link: column.link,
1978
+ editable: true,
1979
+ editableCondition: this.resolveEditableCondition(),
1980
+ bindKey: editableConfig.bindKey || column.sourceColumn || column.dataKey || column.id,
1981
+ editor: editableConfig.editor,
1982
+ options: this.resolveEditableColumnOptions(column.id),
1983
+ includeBlankOption: editableConfig.includeBlankOption,
1984
+ blankOptionLabel: editableConfig.blankOptionLabel,
1985
+ placeholder: editableConfig.placeholder,
1986
+ inputType: editableConfig.inputType,
1987
+ };
1988
+ });
1989
+ }
1990
+ normalizeTableRows(view) {
1991
+ const columns = view.columns || [];
1992
+ const hiddenRowActions = new Set(Array.isArray(this.tab?.props?.['hiddenRowActions'])
1993
+ ? this.tab.props['hiddenRowActions'].map((id) => String(id || '').trim()).filter(Boolean)
1994
+ : []);
1995
+ const rowActions = (view.actions?.row?.custom || []).filter((action) => !hiddenRowActions.has(action.id));
1996
+ const configuredRowActions = view.actions?.row || {};
1997
+ const defaultRowActions = {
1998
+ edit: view.viewActions?.edit !== false && configuredRowActions.edit !== false,
1999
+ editCondition: configuredRowActions.edit && typeof configuredRowActions.edit === 'object'
2000
+ ? configuredRowActions.edit.condition
2001
+ : undefined,
2002
+ delete: view.viewActions?.delete !== false && configuredRowActions.delete !== false,
2003
+ deleteCondition: configuredRowActions.delete && typeof configuredRowActions.delete === 'object'
2004
+ ? configuredRowActions.delete.condition
2005
+ : undefined,
2006
+ };
2007
+ return (view.rows || []).map((row) => this.toTableRowData(row, columns, rowActions, defaultRowActions));
2008
+ }
2009
+ toTableRowData(row, columns, rowActions = [], configuredActions = {}) {
2010
+ const baseRow = Array.isArray(row)
2011
+ ? columns.reduce((acc, column, index) => {
2012
+ acc[column.id] = row[index] ?? '';
2013
+ return acc;
2014
+ }, { id: String(row[0] ?? '') })
2015
+ : {
2016
+ ...row,
2017
+ id: String(row['id'] ?? ''),
2018
+ };
2019
+ if (this.resolveTableSource() === 'invoice-lines' && String(baseRow['line_id'] || '').trim()) {
2020
+ baseRow['id'] = String(baseRow['line_id']).trim();
2021
+ }
2022
+ if (!Array.isArray(baseRow['actions']) && rowActions.length) {
2023
+ baseRow['actions'] = rowActions.map((action) => ({ ...action }));
2024
+ }
2025
+ if (!Array.isArray(baseRow['actions'])) {
2026
+ const actions = [];
2027
+ const editAllowed = configuredActions.edit !== false
2028
+ && (this.isEditEnabled() || evaluateCondition(configuredActions.editCondition, baseRow));
2029
+ if (editAllowed) {
2030
+ actions.push({
2031
+ id: 'edit',
2032
+ label: 'Редагувати',
2033
+ type: ACTION_FORM,
2034
+ formId: this.resolveTableSource(),
2035
+ });
2036
+ }
2037
+ const deleteAllowed = configuredActions.delete !== false
2038
+ && (this.isDeleteEnabled(baseRow) || evaluateCondition(configuredActions.deleteCondition, baseRow));
2039
+ if (deleteAllowed) {
2040
+ actions.push({
2041
+ id: 'delete',
2042
+ label: 'Видалити',
2043
+ type: ACTION_API,
2044
+ api: `/api/${this.resolveTableSource()}/{{id}}`,
2045
+ method: 'DELETE',
2046
+ danger: true,
2047
+ payload: { id: '{{id}}' },
2048
+ });
2049
+ }
2050
+ if (actions.length)
2051
+ baseRow['actions'] = actions;
2052
+ }
2053
+ return baseRow;
2054
+ }
2055
+ saveEditableColumnChange(editableConfig, row) {
2056
+ const api = String(editableConfig.api || '').trim();
2057
+ if (!api) {
2058
+ return;
2059
+ }
2060
+ const payload = this.resolveEditableColumnPayload(editableConfig, row);
2061
+ if (!payload) {
2062
+ return;
2063
+ }
2064
+ this.isTableActionRunningSignal.set(true);
2065
+ this.host?.callApi({
2066
+ api,
2067
+ method: editableConfig.method || 'PATCH',
2068
+ payload,
2069
+ }).subscribe({
2070
+ next: () => {
2071
+ this.isTableActionRunningSignal.set(false);
2072
+ this.notify(editableConfig.successMessage || 'Збережено', 'success');
2073
+ this.loadTable();
2074
+ this.eventBus.emit(ENTITY_UPDATED, {
2075
+ source: 'card-table.editable-cell',
2076
+ });
2077
+ },
2078
+ error: (error) => {
2079
+ this.isTableActionRunningSignal.set(false);
2080
+ this.notify(error?.error?.message || editableConfig.errorMessage || 'Не вдалося зберегти зміни', 'error');
2081
+ },
2082
+ });
2083
+ }
2084
+ loadEditableColumnOptions() {
2085
+ if (this.editableColumnsLoaded) {
2086
+ return;
2087
+ }
2088
+ const editableColumns = this.resolveEditableColumnsConfig();
2089
+ const columnsNeedingOptions = editableColumns.filter((column) => column.optionsSource === 'warehouses' || column.optionsSource === 'warehouse-items');
2090
+ if (!columnsNeedingOptions.length) {
2091
+ this.editableColumnsLoaded = true;
2092
+ return;
2093
+ }
2094
+ this.editableColumnsLoaded = true;
2095
+ this.warehouseService.getWarehouses().subscribe({
2096
+ next: (response) => {
2097
+ const warehouses = response.items ?? [];
2098
+ const warehouseOptions = warehouses
2099
+ .map((warehouse) => ({
2100
+ value: warehouse.id ? String(warehouse.id) : null,
2101
+ label: String(warehouse.name || warehouse.labelName || warehouse.id || 'Склад').trim() || 'Склад',
2102
+ }))
2103
+ .filter((option) => !!option.value);
2104
+ const loadWarehouseItemOptions = (done) => {
2105
+ if (!warehouses.length) {
2106
+ done([]);
2107
+ return;
2108
+ }
2109
+ const options = [];
2110
+ let remaining = warehouses.length;
2111
+ for (const warehouse of warehouses) {
2112
+ const warehouseId = String(warehouse.id || '').trim();
2113
+ if (!warehouseId) {
2114
+ remaining -= 1;
2115
+ continue;
2116
+ }
2117
+ this.warehouseService.getWarehouseItems(warehouseId).subscribe({
2118
+ next: (items) => {
2119
+ for (const item of items ?? []) {
2120
+ options.push({
2121
+ value: item.id ? String(item.id) : null,
2122
+ label: [
2123
+ String(warehouse.labelName || warehouse.name || warehouse.id || '').trim(),
2124
+ String(item.name || item.id || '').trim(),
2125
+ ]
2126
+ .filter(Boolean)
2127
+ .join(' / '),
2128
+ });
2129
+ }
2130
+ remaining -= 1;
2131
+ if (remaining <= 0) {
2132
+ done(options.filter((option) => !!option.value));
2133
+ }
2134
+ },
2135
+ error: () => {
2136
+ remaining -= 1;
2137
+ if (remaining <= 0) {
2138
+ done(options.filter((option) => !!option.value));
2139
+ }
2140
+ },
2141
+ });
2142
+ }
2143
+ };
2144
+ const applyOptions = (warehouseItemOptions = []) => {
2145
+ this.editableColumnOptionsSignal.set(columnsNeedingOptions.reduce((acc, column) => {
2146
+ acc[column.key] = column.optionsSource === 'warehouses' ? warehouseOptions : warehouseItemOptions;
2147
+ return acc;
2148
+ }, {}));
2149
+ this.tableColumnsSignal.set(this.normalizeTableColumns(this.tableViewColumns));
2150
+ };
2151
+ if (!columnsNeedingOptions.some((column) => column.optionsSource === 'warehouse-items')) {
2152
+ applyOptions();
2153
+ return;
2154
+ }
2155
+ loadWarehouseItemOptions(applyOptions);
2156
+ },
2157
+ error: () => {
2158
+ this.editableColumnOptionsSignal.set({});
2159
+ this.tableColumnsSignal.set(this.normalizeTableColumns(this.tableViewColumns));
2160
+ },
2161
+ });
2162
+ }
2163
+ resolveEditableColumnsConfig() {
2164
+ const value = this.tab?.props?.['editableColumns'];
2165
+ return Array.isArray(value)
2166
+ ? value.filter((item) => !!item && typeof item === 'object' && typeof item.key === 'string')
2167
+ : [];
2168
+ }
2169
+ resolveEditableColumnPayload(editableConfig, row) {
2170
+ const baseRow = {
2171
+ ...row,
2172
+ id: String(row['id'] ?? '').trim(),
2173
+ };
2174
+ const payloadConfig = editableConfig.payload && typeof editableConfig.payload === 'object' ? editableConfig.payload : {};
2175
+ const payload = this.resolveTemplatePayload(payloadConfig, {
2176
+ ...this.data,
2177
+ ...baseRow,
2178
+ });
2179
+ if (!Object.keys(payload).length) {
2180
+ return baseRow;
2181
+ }
2182
+ return {
2183
+ ...baseRow,
2184
+ ...payload,
2185
+ };
2186
+ }
2187
+ resolveEditableColumnOptions(columnKey) {
2188
+ return this.editableColumnOptionsSignal()[columnKey] || [];
2189
+ }
2190
+ isEditEnabled() {
2191
+ const value = this.tab?.props?.['edit'];
2192
+ const enabled = value === true || String(value).trim().toLowerCase() === 'true';
2193
+ if (!enabled) {
2194
+ return false;
2195
+ }
2196
+ return evaluateCondition(String(this.tab?.props?.['editCondition'] || '').trim(), this.data || {});
2197
+ }
2198
+ isDeleteEnabled(row) {
2199
+ const value = this.tab?.props?.['delete'];
2200
+ const enabled = value === true || String(value).trim().toLowerCase() === 'true';
2201
+ if (!enabled)
2202
+ return false;
2203
+ return evaluateCondition(String(this.tab?.props?.['deleteCondition'] || '').trim(), row);
2204
+ }
2205
+ resolveTableSource() {
2206
+ return this.resolveTemplateString(String(this.tab?.props?.['table'] || '').trim(), this.data || {});
2207
+ }
2208
+ resolveTableFilters() {
2209
+ return {
2210
+ ...this.resolveFiltersObject(this.tab?.props?.['initialFilters']),
2211
+ ...this.resolveFiltersObject(this.tab?.props?.['filter']),
2212
+ };
2213
+ }
2214
+ resolveFiltersObject(filter) {
2215
+ if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
2216
+ return {};
2217
+ }
2218
+ return this.resolveTemplatePayload(filter, this.data || {});
2219
+ }
2220
+ resolveTemplatePayload(payload, source) {
2221
+ return Object.entries(payload || {}).reduce((acc, [key, value]) => {
2222
+ const next = this.resolveTemplateValue(value, source);
2223
+ if (next !== null && next !== undefined && next !== '') {
2224
+ acc[key] = next;
2225
+ }
2226
+ return acc;
2227
+ }, {});
2228
+ }
2229
+ resolveTemplateString(value, source) {
2230
+ return String(value || '').replace(/\{\{([a-zA-Z_][a-zA-Z0-9_.]*)\}\}/g, (_match, key) => {
2231
+ const next = String(key).split('.').reduce((current, part) => {
2232
+ if (current == null || typeof current !== 'object')
2233
+ return undefined;
2234
+ return current[part];
2235
+ }, source);
2236
+ return next == null || next === '' ? '' : encodeURIComponent(String(next));
2237
+ });
2238
+ }
2239
+ resolveTemplateValue(value, source) {
2240
+ if (typeof value !== 'string') {
2241
+ return value;
2242
+ }
2243
+ const raw = value.trim();
2244
+ const match = raw.match(/^\{\{([a-zA-Z_][a-zA-Z0-9_.]*)\}\}$/);
2245
+ if (match) {
2246
+ return String(match[1]).split('.').reduce((current, part) => {
2247
+ if (current == null || typeof current !== 'object')
2248
+ return undefined;
2249
+ return current[part];
2250
+ }, source) ?? '';
2251
+ }
2252
+ return value;
2253
+ }
2254
+ resolvePageSize() {
2255
+ const value = Number(this.tab?.props?.['pageSize'] ?? 10);
2256
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 10;
2257
+ }
2258
+ resolveEditCondition() {
2259
+ const condition = String(this.tab?.props?.['editCondition'] || '').trim();
2260
+ return condition || undefined;
2261
+ }
2262
+ resolveEditableCondition() {
2263
+ const condition = String(this.tab?.props?.['editableCondition'] || '').trim();
2264
+ return condition || this.resolveEditCondition();
2265
+ }
2266
+ notify(message, variant) {
2267
+ this.host?.notify(message, variant, 2800);
2268
+ }
2269
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2270
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: CardTableComponent, isStandalone: true, selector: "app-card-table", inputs: { tab: "tab", data: "data" }, usesOnChanges: true, ngImport: i0, template: "<section class=\"space-y-4\">\n @if (tableHeaderActions.length) {\n <div class=\"flex justify-end gap-2\">\n @for (action of tableHeaderActions; track action.id) {\n <ui-button\n [variant]=\"action.variant || 'ghost'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n type=\"button\"\n (click)=\"onHeaderAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n\n <app-ui-table\n [data]=\"tableData()\"\n [columns]=\"tableColumns\"\n [isLoading]=\"isTableLoading\"\n [disablePaging]=\"disablePaging\"\n [rowClickable]=\"rowClickable\"\n (pageChange)=\"onTablePageChange($event)\"\n (actionClick)=\"onTableAction($event)\"\n (rowClick)=\"onTableRowClick($event)\"\n (cellChange)=\"onTableCellChange($event)\"\n ></app-ui-table>\n\n @if (activeFormTarget(); as target) {\n <forms-action-form-modal\n [isOpen]=\"true\"\n [formId]=\"target.formId\"\n [recordId]=\"target.recordId\"\n [initialValue]=\"target.initialValue\"\n (closed)=\"closeFormAction()\"\n (completed)=\"refreshTable()\"\n ></forms-action-form-modal>\n }\n</section>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: UiTableComponent, selector: "app-ui-table", inputs: ["columns", "isLoading", "data", "disablePaging", "rowClickable", "imageResolver"], outputs: ["pageChange", "rowClick", "actionClick", "cellChange"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: UiActionFormModalComponent, selector: "forms-action-form-modal", inputs: ["formId", "recordId", "size", "isOpen", "cancelLabel", "initialValue"], outputs: ["closed", "completed"] }] });
2271
+ }
2272
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CardTableComponent, decorators: [{
2273
+ type: Component,
2274
+ args: [{ selector: 'app-card-table', standalone: true, imports: [
2275
+ CommonModule,
2276
+ UiTableComponent,
2277
+ UiButtonComponent,
2278
+ UiActionFormModalComponent,
2279
+ ], template: "<section class=\"space-y-4\">\n @if (tableHeaderActions.length) {\n <div class=\"flex justify-end gap-2\">\n @for (action of tableHeaderActions; track action.id) {\n <ui-button\n [variant]=\"action.variant || 'ghost'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n type=\"button\"\n (click)=\"onHeaderAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n\n <app-ui-table\n [data]=\"tableData()\"\n [columns]=\"tableColumns\"\n [isLoading]=\"isTableLoading\"\n [disablePaging]=\"disablePaging\"\n [rowClickable]=\"rowClickable\"\n (pageChange)=\"onTablePageChange($event)\"\n (actionClick)=\"onTableAction($event)\"\n (rowClick)=\"onTableRowClick($event)\"\n (cellChange)=\"onTableCellChange($event)\"\n ></app-ui-table>\n\n @if (activeFormTarget(); as target) {\n <forms-action-form-modal\n [isOpen]=\"true\"\n [formId]=\"target.formId\"\n [recordId]=\"target.recordId\"\n [initialValue]=\"target.initialValue\"\n (closed)=\"closeFormAction()\"\n (completed)=\"refreshTable()\"\n ></forms-action-form-modal>\n }\n</section>\n" }]
2280
+ }], propDecorators: { tab: [{
2281
+ type: Input,
2282
+ args: [{ required: true }]
2283
+ }], data: [{
2284
+ type: Input
2285
+ }] } });
2286
+
607
2287
  /**
608
2288
  * Generated bundle index. Do not edit.
609
2289
  */
610
2290
 
611
- export { CARD_ACTION_COMPLETED, CARD_ACTION_FINISHED, CARD_ACTION_REQUESTED, CARD_ACTION_STARTED, CardComponent, CardSectionHeaderComponent, ENTITY_UPDATED, ImagesViewerComponent, UI_CARD_TAB_REGISTRY, UI_ENTITY_CARD_HOST };
2291
+ export { ACTION_API, ACTION_FORM, ACTION_PRINT, CARD_ACTION_COMPLETED, CARD_ACTION_FINISHED, CARD_ACTION_REQUESTED, CARD_ACTION_STARTED, CardComponent, CardCurrentUserService, CardDictionaryService, CardImageService, CardSectionHeaderComponent, CardTableBarcodeService, CardTableComponent, CardTableDataService, CardTablePrintService, CardTableWarehouseService, DocumentsComponent, ENTITY_UPDATED, HistoryTabComponent, ImagesViewerComponent, UI_CARD_API_BASE_URL, UI_CARD_TAB_REGISTRY, UI_ENTITY_CARD_HOST, UiEntityDocumentsService, cardApiParams, cardApiResourceUrl, normalizeActionType };
612
2292
  //# sourceMappingURL=maro-tech-card.mjs.map