2026-02-07 10:23:29 -04:00
|
|
|
import { Component, inject, signal, computed } from '@angular/core';
|
|
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
|
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
|
|
|
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
|
|
|
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
|
|
|
import { MatSelectModule } from '@angular/material/select';
|
|
|
|
|
import { MatInputModule } from '@angular/material/input';
|
|
|
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
|
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
|
|
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
|
|
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
|
|
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
|
|
|
import { MatChipsModule } from '@angular/material/chips';
|
|
|
|
|
import { Clipboard } from '@angular/cdk/clipboard';
|
|
|
|
|
import { DocumentService } from '../services/document.service';
|
|
|
|
|
import { NotificationService } from '../../../core/services/notification.service';
|
|
|
|
|
import { DocumentType, DocumentResponseDto } from '../../../api/models';
|
2026-02-08 02:10:09 -04:00
|
|
|
import {
|
|
|
|
|
INPUT_LIMITS,
|
|
|
|
|
noScriptValidator,
|
|
|
|
|
noNullBytesValidator,
|
|
|
|
|
normalizeWhitespace,
|
|
|
|
|
} from '../../../shared/utils/form-validators';
|
|
|
|
|
import { createSubmitDebounce } from '../../../shared/utils/form-utils';
|
2026-02-07 10:23:29 -04:00
|
|
|
|
|
|
|
|
export interface DocumentUploadDialogData {
|
|
|
|
|
requestId: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type UploadState = 'idle' | 'uploading' | 'processing' | 'complete' | 'error';
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
selector: 'app-document-upload',
|
|
|
|
|
standalone: true,
|
|
|
|
|
imports: [
|
|
|
|
|
CommonModule,
|
|
|
|
|
ReactiveFormsModule,
|
|
|
|
|
MatDialogModule,
|
|
|
|
|
MatFormFieldModule,
|
|
|
|
|
MatSelectModule,
|
|
|
|
|
MatInputModule,
|
|
|
|
|
MatButtonModule,
|
|
|
|
|
MatIconModule,
|
|
|
|
|
MatProgressBarModule,
|
|
|
|
|
MatProgressSpinnerModule,
|
|
|
|
|
MatTooltipModule,
|
|
|
|
|
MatChipsModule,
|
|
|
|
|
],
|
|
|
|
|
template: `
|
|
|
|
|
<div class="upload-dialog">
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
<div class="dialog-header">
|
|
|
|
|
<div class="header-icon">
|
|
|
|
|
<mat-icon>cloud_upload</mat-icon>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="header-text">
|
|
|
|
|
<h2>Upload Document</h2>
|
|
|
|
|
<p>Add a document to your request</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<mat-dialog-content>
|
|
|
|
|
@if (uploadState() === 'complete' && uploadedDocument()) {
|
|
|
|
|
<!-- Success State -->
|
|
|
|
|
<div class="success-state">
|
|
|
|
|
<div class="success-icon">
|
|
|
|
|
<mat-icon>check_circle</mat-icon>
|
|
|
|
|
</div>
|
|
|
|
|
<h3>Document Uploaded Successfully</h3>
|
|
|
|
|
<p class="success-message">Your document has been securely uploaded and hashed on the blockchain.</p>
|
|
|
|
|
|
|
|
|
|
<!-- Document Info Card -->
|
|
|
|
|
<div class="document-card">
|
|
|
|
|
<div class="doc-preview">
|
|
|
|
|
@if (isImageFile()) {
|
|
|
|
|
<img [src]="previewUrl()" alt="Document preview" class="preview-image" />
|
|
|
|
|
} @else {
|
|
|
|
|
<div class="file-icon-large">
|
|
|
|
|
<mat-icon>{{ getFileIcon() }}</mat-icon>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="doc-info">
|
|
|
|
|
<div class="doc-name">{{ uploadedDocument()!.originalFilename }}</div>
|
|
|
|
|
<div class="doc-meta">
|
|
|
|
|
<span class="meta-item">
|
|
|
|
|
<mat-icon>folder</mat-icon>
|
|
|
|
|
{{ formatDocType(uploadedDocument()!.docType) }}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="meta-item">
|
|
|
|
|
<mat-icon>schedule</mat-icon>
|
|
|
|
|
Just now
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Blockchain Hash Display -->
|
|
|
|
|
<div class="hash-section">
|
|
|
|
|
<div class="hash-header">
|
|
|
|
|
<mat-icon>fingerprint</mat-icon>
|
|
|
|
|
<span>Document Hash (SHA-256)</span>
|
|
|
|
|
<div class="verified-badge">
|
|
|
|
|
<mat-icon>verified</mat-icon>
|
|
|
|
|
Blockchain Verified
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="hash-display">
|
|
|
|
|
<code class="hash-value">{{ uploadedDocument()!.currentHash }}</code>
|
|
|
|
|
<button
|
|
|
|
|
mat-icon-button
|
|
|
|
|
(click)="copyHash()"
|
|
|
|
|
matTooltip="Copy hash to clipboard"
|
|
|
|
|
class="copy-btn"
|
|
|
|
|
>
|
|
|
|
|
<mat-icon>{{ hashCopied() ? 'check' : 'content_copy' }}</mat-icon>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="hash-hint">
|
|
|
|
|
This cryptographic hash uniquely identifies your document and is permanently recorded on the blockchain.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
} @else {
|
|
|
|
|
<!-- Upload Form -->
|
|
|
|
|
<form [formGroup]="form" class="upload-form">
|
|
|
|
|
<!-- Document Type -->
|
|
|
|
|
<mat-form-field appearance="outline" class="full-width">
|
|
|
|
|
<mat-label>Document Type</mat-label>
|
|
|
|
|
<mat-icon matPrefix>category</mat-icon>
|
|
|
|
|
<mat-select formControlName="docType">
|
|
|
|
|
@for (type of documentTypes; track type.value) {
|
|
|
|
|
<mat-option [value]="type.value">
|
|
|
|
|
<div class="type-option">
|
|
|
|
|
<mat-icon>{{ type.icon }}</mat-icon>
|
|
|
|
|
{{ type.label }}
|
|
|
|
|
</div>
|
|
|
|
|
</mat-option>
|
|
|
|
|
}
|
|
|
|
|
</mat-select>
|
|
|
|
|
@if (form.controls.docType.hasError('required')) {
|
|
|
|
|
<mat-error>Please select a document type</mat-error>
|
|
|
|
|
}
|
|
|
|
|
</mat-form-field>
|
|
|
|
|
|
|
|
|
|
<!-- Description -->
|
|
|
|
|
<mat-form-field appearance="outline" class="full-width">
|
|
|
|
|
<mat-label>Description (optional)</mat-label>
|
|
|
|
|
<mat-icon matPrefix>notes</mat-icon>
|
|
|
|
|
<textarea
|
|
|
|
|
matInput
|
|
|
|
|
formControlName="description"
|
|
|
|
|
rows="2"
|
|
|
|
|
placeholder="Add any additional notes about this document"
|
2026-02-08 02:10:09 -04:00
|
|
|
[maxlength]="limits.DESCRIPTION_MAX"
|
2026-02-07 10:23:29 -04:00
|
|
|
></textarea>
|
2026-02-08 02:10:09 -04:00
|
|
|
@if (form.controls.description.hasError('maxlength')) {
|
|
|
|
|
<mat-error>Maximum {{ limits.DESCRIPTION_MAX }} characters allowed</mat-error>
|
|
|
|
|
}
|
|
|
|
|
@if (form.controls.description.hasError('dangerousContent')) {
|
|
|
|
|
<mat-error>Invalid characters detected</mat-error>
|
|
|
|
|
}
|
|
|
|
|
<mat-hint align="end">{{ form.controls.description.value?.length || 0 }}/{{ limits.DESCRIPTION_MAX }}</mat-hint>
|
2026-02-07 10:23:29 -04:00
|
|
|
</mat-form-field>
|
|
|
|
|
|
|
|
|
|
<!-- Drop Zone -->
|
|
|
|
|
<div
|
|
|
|
|
class="drop-zone"
|
|
|
|
|
[class.has-file]="selectedFile()"
|
|
|
|
|
[class.drag-over]="isDragOver()"
|
|
|
|
|
[class.uploading]="uploadState() === 'uploading'"
|
|
|
|
|
[class.error]="uploadState() === 'error'"
|
|
|
|
|
(dragover)="onDragOver($event)"
|
|
|
|
|
(dragleave)="onDragLeave($event)"
|
|
|
|
|
(drop)="onDrop($event)"
|
|
|
|
|
(click)="uploadState() !== 'uploading' && fileInput.click()"
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
#fileInput
|
|
|
|
|
type="file"
|
|
|
|
|
hidden
|
|
|
|
|
accept=".pdf,.jpg,.jpeg,.png,.doc,.docx"
|
|
|
|
|
(change)="onFileSelected($event)"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
@if (uploadState() === 'uploading' || uploadState() === 'processing') {
|
|
|
|
|
<!-- Uploading State -->
|
|
|
|
|
<div class="upload-progress-container">
|
|
|
|
|
<div class="progress-circle">
|
|
|
|
|
<svg viewBox="0 0 100 100">
|
|
|
|
|
<circle class="progress-bg" cx="50" cy="50" r="45" />
|
|
|
|
|
<circle
|
|
|
|
|
class="progress-fill"
|
|
|
|
|
cx="50"
|
|
|
|
|
cy="50"
|
|
|
|
|
r="45"
|
|
|
|
|
[style.stroke-dashoffset]="getProgressOffset()"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
<div class="progress-text">
|
|
|
|
|
@if (uploadState() === 'processing') {
|
|
|
|
|
<mat-icon class="processing-icon">sync</mat-icon>
|
|
|
|
|
} @else {
|
|
|
|
|
<span class="percentage">{{ uploadProgress() }}%</span>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="upload-status">
|
|
|
|
|
@if (uploadState() === 'processing') {
|
|
|
|
|
<span class="status-text">Generating blockchain hash...</span>
|
|
|
|
|
} @else {
|
|
|
|
|
<span class="status-text">Uploading {{ selectedFile()?.name }}</span>
|
|
|
|
|
<span class="status-detail">{{ formatBytes(uploadedBytes()) }} / {{ formatBytes(totalBytes()) }}</span>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
} @else if (selectedFile()) {
|
|
|
|
|
<!-- File Selected State -->
|
|
|
|
|
<div class="file-selected">
|
|
|
|
|
@if (isImageFile()) {
|
|
|
|
|
<img [src]="previewUrl()" alt="Preview" class="preview-thumb" />
|
|
|
|
|
} @else {
|
|
|
|
|
<div class="file-icon">
|
|
|
|
|
<mat-icon>{{ getFileIcon() }}</mat-icon>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
<div class="file-details">
|
|
|
|
|
<span class="file-name">{{ selectedFile()!.name }}</span>
|
|
|
|
|
<span class="file-size">{{ formatFileSize(selectedFile()!.size) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
mat-icon-button
|
|
|
|
|
(click)="clearFile($event)"
|
|
|
|
|
matTooltip="Remove file"
|
|
|
|
|
class="remove-btn"
|
|
|
|
|
>
|
|
|
|
|
<mat-icon>close</mat-icon>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
} @else {
|
|
|
|
|
<!-- Empty State -->
|
|
|
|
|
<div class="empty-state">
|
|
|
|
|
<div class="upload-icon-wrapper">
|
|
|
|
|
<mat-icon>cloud_upload</mat-icon>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="primary-text">Drag & drop a file here</span>
|
|
|
|
|
<span class="secondary-text">or click to browse</span>
|
|
|
|
|
<div class="file-types">
|
|
|
|
|
<mat-chip-set>
|
|
|
|
|
<mat-chip>PDF</mat-chip>
|
|
|
|
|
<mat-chip>JPG</mat-chip>
|
|
|
|
|
<mat-chip>PNG</mat-chip>
|
|
|
|
|
<mat-chip>DOC</mat-chip>
|
|
|
|
|
</mat-chip-set>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="size-limit">Maximum file size: 10MB</span>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@if (uploadState() === 'error') {
|
|
|
|
|
<div class="error-message">
|
|
|
|
|
<mat-icon>error</mat-icon>
|
|
|
|
|
<span>{{ errorMessage() }}</span>
|
|
|
|
|
<button mat-button color="primary" (click)="resetUpload()">Try Again</button>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<!-- Progress Bar (linear backup) -->
|
|
|
|
|
@if (uploadState() === 'uploading') {
|
|
|
|
|
<mat-progress-bar
|
|
|
|
|
mode="determinate"
|
|
|
|
|
[value]="uploadProgress()"
|
|
|
|
|
class="linear-progress"
|
|
|
|
|
></mat-progress-bar>
|
|
|
|
|
}
|
|
|
|
|
</form>
|
|
|
|
|
}
|
|
|
|
|
</mat-dialog-content>
|
|
|
|
|
|
|
|
|
|
<mat-dialog-actions align="end">
|
|
|
|
|
@if (uploadState() === 'complete') {
|
|
|
|
|
<button mat-flat-button color="primary" (click)="onDone()">
|
|
|
|
|
<mat-icon>check</mat-icon>
|
|
|
|
|
Done
|
|
|
|
|
</button>
|
|
|
|
|
} @else {
|
|
|
|
|
<button mat-button (click)="onCancel()" [disabled]="uploadState() === 'uploading'">
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
mat-flat-button
|
|
|
|
|
color="primary"
|
|
|
|
|
(click)="onUpload()"
|
|
|
|
|
[disabled]="!canUpload() || uploadState() === 'uploading'"
|
|
|
|
|
>
|
|
|
|
|
@if (uploadState() === 'uploading') {
|
|
|
|
|
<mat-spinner diameter="20" class="btn-spinner"></mat-spinner>
|
|
|
|
|
Uploading...
|
|
|
|
|
} @else {
|
|
|
|
|
<ng-container>
|
|
|
|
|
<mat-icon>upload</mat-icon>
|
|
|
|
|
Upload Document
|
|
|
|
|
</ng-container>
|
|
|
|
|
}
|
|
|
|
|
</button>
|
|
|
|
|
}
|
|
|
|
|
</mat-dialog-actions>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
styles: [`
|
|
|
|
|
.upload-dialog {
|
|
|
|
|
min-width: 480px;
|
|
|
|
|
max-width: 560px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
background: linear-gradient(135deg, var(--dbim-blue-dark, #1D0A69) 0%, var(--dbim-blue-mid, #2563EB) 100%);
|
|
|
|
|
margin: -24px -24px 24px -24px;
|
|
|
|
|
color: white;
|
|
|
|
|
|
|
|
|
|
.header-icon {
|
|
|
|
|
width: 48px;
|
|
|
|
|
height: 48px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
|
|
mat-icon {
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
width: 28px;
|
|
|
|
|
height: 28px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-text {
|
|
|
|
|
h2 {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
p {
|
|
|
|
|
margin: 4px 0 0;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
opacity: 0.85;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upload-form {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.full-width {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.type-option {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
|
|
mat-icon {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
width: 18px;
|
|
|
|
|
height: 18px;
|
|
|
|
|
color: var(--dbim-grey-2, #8E8E8E);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Drop Zone */
|
|
|
|
|
.drop-zone {
|
|
|
|
|
border: 2px dashed var(--dbim-grey-1, #C6C6C6);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 32px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
background: var(--dbim-linen, #EBEAEA);
|
|
|
|
|
min-height: 180px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
|
|
&:hover:not(.uploading) {
|
|
|
|
|
border-color: var(--dbim-blue-mid, #2563EB);
|
|
|
|
|
background: rgba(37, 99, 235, 0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.drag-over {
|
|
|
|
|
border-color: var(--dbim-blue-mid, #2563EB);
|
|
|
|
|
background: rgba(37, 99, 235, 0.1);
|
|
|
|
|
transform: scale(1.01);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.has-file {
|
|
|
|
|
border-style: solid;
|
|
|
|
|
border-color: var(--dbim-success, #198754);
|
|
|
|
|
background: rgba(25, 135, 84, 0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.uploading {
|
|
|
|
|
cursor: default;
|
|
|
|
|
border-color: var(--dbim-blue-mid, #2563EB);
|
|
|
|
|
background: rgba(37, 99, 235, 0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.error {
|
|
|
|
|
border-color: var(--dbim-error, #DC3545);
|
|
|
|
|
background: rgba(220, 53, 69, 0.05);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-state {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
|
|
.upload-icon-wrapper {
|
|
|
|
|
width: 64px;
|
|
|
|
|
height: 64px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: linear-gradient(135deg, var(--dbim-blue-dark, #1D0A69), var(--dbim-blue-mid, #2563EB));
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
|
|
|
|
mat-icon {
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.primary-text {
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--dbim-brown, #150202);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.secondary-text {
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
color: var(--dbim-grey-2, #8E8E8E);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-types {
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
|
|
|
|
mat-chip {
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
min-height: 24px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.size-limit {
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
color: var(--dbim-grey-2, #8E8E8E);
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-selected {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
|
|
.preview-thumb {
|
|
|
|
|
width: 64px;
|
|
|
|
|
height: 64px;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid var(--dbim-grey-1, #C6C6C6);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-icon {
|
|
|
|
|
width: 64px;
|
|
|
|
|
height: 64px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
background: var(--dbim-blue-subtle, #DBEAFE);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
|
|
mat-icon {
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
color: var(--dbim-blue-mid, #2563EB);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-details {
|
|
|
|
|
flex: 1;
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
|
|
|
|
.file-name {
|
|
|
|
|
display: block;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--dbim-brown, #150202);
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-size {
|
|
|
|
|
display: block;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
color: var(--dbim-grey-2, #8E8E8E);
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remove-btn {
|
|
|
|
|
color: var(--dbim-grey-2, #8E8E8E);
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
color: var(--dbim-error, #DC3545);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Upload Progress */
|
|
|
|
|
.upload-progress-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
|
|
|
|
.progress-circle {
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 100px;
|
|
|
|
|
height: 100px;
|
|
|
|
|
|
|
|
|
|
svg {
|
|
|
|
|
transform: rotate(-90deg);
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-bg {
|
|
|
|
|
fill: none;
|
|
|
|
|
stroke: var(--dbim-grey-1, #C6C6C6);
|
|
|
|
|
stroke-width: 8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-fill {
|
|
|
|
|
fill: none;
|
|
|
|
|
stroke: var(--dbim-blue-mid, #2563EB);
|
|
|
|
|
stroke-width: 8;
|
|
|
|
|
stroke-linecap: round;
|
|
|
|
|
stroke-dasharray: 283;
|
|
|
|
|
transition: stroke-dashoffset 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-text {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
|
|
.percentage {
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--dbim-blue-mid, #2563EB);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.processing-icon {
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
color: var(--dbim-blue-mid, #2563EB);
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upload-status {
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
|
|
|
|
.status-text {
|
|
|
|
|
display: block;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--dbim-brown, #150202);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-detail {
|
|
|
|
|
display: block;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
color: var(--dbim-grey-2, #8E8E8E);
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
from { transform: rotate(0deg); }
|
|
|
|
|
to { transform: rotate(360deg); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.linear-progress {
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.error-message {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
background: rgba(220, 53, 69, 0.1);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
color: var(--dbim-error, #DC3545);
|
|
|
|
|
|
|
|
|
|
mat-icon {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
width: 20px;
|
|
|
|
|
height: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
flex: 1;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Success State */
|
|
|
|
|
.success-state {
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
|
|
|
|
.success-icon {
|
|
|
|
|
width: 72px;
|
|
|
|
|
height: 72px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: rgba(25, 135, 84, 0.1);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
margin: 0 auto 16px;
|
|
|
|
|
|
|
|
|
|
mat-icon {
|
|
|
|
|
font-size: 40px;
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
color: var(--dbim-success, #198754);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h3 {
|
|
|
|
|
margin: 0 0 8px;
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
color: var(--dbim-brown, #150202);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.success-message {
|
|
|
|
|
margin: 0 0 24px;
|
|
|
|
|
color: var(--dbim-grey-2, #8E8E8E);
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.document-card {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
background: var(--dbim-linen, #EBEAEA);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
|
|
|
|
.doc-preview {
|
|
|
|
|
.preview-image {
|
|
|
|
|
width: 64px;
|
|
|
|
|
height: 64px;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-icon-large {
|
|
|
|
|
width: 64px;
|
|
|
|
|
height: 64px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
background: var(--dbim-blue-subtle, #DBEAFE);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
|
|
mat-icon {
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
color: var(--dbim-blue-mid, #2563EB);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.doc-info {
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
|
|
.doc-name {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--dbim-brown, #150202);
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.doc-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
|
|
|
|
.meta-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
color: var(--dbim-grey-2, #8E8E8E);
|
|
|
|
|
|
|
|
|
|
mat-icon {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
width: 14px;
|
|
|
|
|
height: 14px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hash-section {
|
|
|
|
|
background: linear-gradient(135deg, rgba(29, 10, 105, 0.05), rgba(37, 99, 235, 0.05));
|
|
|
|
|
border: 1px solid rgba(37, 99, 235, 0.2);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
|
|
|
|
.hash-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
|
|
|
|
mat-icon {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
width: 20px;
|
|
|
|
|
height: 20px;
|
|
|
|
|
color: var(--dbim-blue-mid, #2563EB);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--dbim-brown, #150202);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.verified-badge {
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
background: rgba(25, 135, 84, 0.1);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
color: var(--dbim-success, #198754);
|
|
|
|
|
|
|
|
|
|
mat-icon {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
width: 14px;
|
|
|
|
|
height: 14px;
|
|
|
|
|
color: var(--dbim-success, #198754);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hash-display {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
border: 1px solid var(--dbim-grey-1, #C6C6C6);
|
|
|
|
|
|
|
|
|
|
.hash-value {
|
|
|
|
|
flex: 1;
|
|
|
|
|
font-family: 'Monaco', 'Consolas', monospace;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
color: var(--dbim-blue-dark, #1D0A69);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.copy-btn {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
width: 36px;
|
|
|
|
|
height: 36px;
|
|
|
|
|
color: var(--dbim-blue-mid, #2563EB);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hash-hint {
|
|
|
|
|
margin: 12px 0 0;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
color: var(--dbim-grey-2, #8E8E8E);
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-spinner {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mat-dialog-actions {
|
|
|
|
|
padding: 16px 24px !important;
|
|
|
|
|
margin: 0 -24px -24px !important;
|
|
|
|
|
border-top: 1px solid var(--dbim-linen, #EBEAEA);
|
|
|
|
|
}
|
|
|
|
|
`],
|
|
|
|
|
})
|
|
|
|
|
export class DocumentUploadComponent {
|
|
|
|
|
private readonly fb = inject(FormBuilder);
|
|
|
|
|
private readonly documentService = inject(DocumentService);
|
|
|
|
|
private readonly notification = inject(NotificationService);
|
|
|
|
|
private readonly dialogRef = inject(MatDialogRef<DocumentUploadComponent>);
|
|
|
|
|
private readonly data: DocumentUploadDialogData = inject(MAT_DIALOG_DATA);
|
|
|
|
|
private readonly clipboard = inject(Clipboard);
|
|
|
|
|
|
2026-02-08 02:10:09 -04:00
|
|
|
/** Debounce handler to prevent double-click submissions */
|
|
|
|
|
private readonly submitDebounce = createSubmitDebounce(500);
|
|
|
|
|
|
|
|
|
|
/** Input limits exposed for template binding */
|
|
|
|
|
readonly limits = INPUT_LIMITS;
|
|
|
|
|
|
2026-02-07 10:23:29 -04:00
|
|
|
// State signals
|
|
|
|
|
readonly uploadState = signal<UploadState>('idle');
|
|
|
|
|
readonly selectedFile = signal<File | null>(null);
|
|
|
|
|
readonly isDragOver = signal(false);
|
|
|
|
|
readonly uploadProgress = signal(0);
|
|
|
|
|
readonly uploadedBytes = signal(0);
|
|
|
|
|
readonly totalBytes = signal(0);
|
|
|
|
|
readonly uploadedDocument = signal<DocumentResponseDto | null>(null);
|
|
|
|
|
readonly errorMessage = signal('');
|
|
|
|
|
readonly previewUrl = signal<string | null>(null);
|
|
|
|
|
readonly hashCopied = signal(false);
|
|
|
|
|
|
|
|
|
|
readonly documentTypes: { value: DocumentType; label: string; icon: string }[] = [
|
2026-02-08 18:44:05 -04:00
|
|
|
{ value: 'ID_PROOF', label: 'Identity Proof', icon: 'badge' },
|
2026-02-07 10:23:29 -04:00
|
|
|
{ value: 'ADDRESS_PROOF', label: 'Address Proof', icon: 'location_on' },
|
2026-02-08 18:44:05 -04:00
|
|
|
{ value: 'FIRE_SAFETY', label: 'Fire Safety Certificate', icon: 'local_fire_department' },
|
|
|
|
|
{ value: 'FLOOR_PLAN', label: 'Floor Plan', icon: 'apartment' },
|
|
|
|
|
{ value: 'SITE_PLAN', label: 'Site Plan', icon: 'map' },
|
|
|
|
|
{ value: 'BUILDING_PERMIT', label: 'Building Permit', icon: 'home_work' },
|
|
|
|
|
{ value: 'BUSINESS_LICENSE', label: 'Business License', icon: 'storefront' },
|
|
|
|
|
{ value: 'PHOTOGRAPH', label: 'Photograph', icon: 'photo_camera' },
|
|
|
|
|
{ value: 'NOC', label: 'No Objection Certificate', icon: 'verified' },
|
|
|
|
|
{ value: 'LICENSE_COPY', label: 'License Copy', icon: 'file_copy' },
|
|
|
|
|
{ value: 'HEALTH_CERT', label: 'Health Certificate', icon: 'health_and_safety' },
|
|
|
|
|
{ value: 'TAX_CLEARANCE', label: 'Tax Clearance', icon: 'receipt_long' },
|
2026-02-07 10:23:29 -04:00
|
|
|
{ value: 'OTHER', label: 'Other Document', icon: 'description' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
readonly form = this.fb.nonNullable.group({
|
|
|
|
|
docType: ['' as DocumentType, [Validators.required]],
|
2026-02-08 02:10:09 -04:00
|
|
|
description: ['', [
|
|
|
|
|
Validators.maxLength(INPUT_LIMITS.DESCRIPTION_MAX),
|
|
|
|
|
noScriptValidator(),
|
|
|
|
|
noNullBytesValidator(),
|
|
|
|
|
]],
|
2026-02-07 10:23:29 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
canUpload(): boolean {
|
|
|
|
|
return this.form.valid && this.selectedFile() !== null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isImageFile(): boolean {
|
|
|
|
|
const file = this.selectedFile();
|
|
|
|
|
if (!file) return false;
|
|
|
|
|
return file.type.startsWith('image/');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getFileIcon(): string {
|
|
|
|
|
const file = this.selectedFile() || this.uploadedDocument();
|
|
|
|
|
if (!file) return 'description';
|
|
|
|
|
|
|
|
|
|
const filename = 'name' in file ? file.name : file.originalFilename;
|
|
|
|
|
const ext = filename.split('.').pop()?.toLowerCase();
|
|
|
|
|
|
|
|
|
|
switch (ext) {
|
|
|
|
|
case 'pdf': return 'picture_as_pdf';
|
|
|
|
|
case 'doc':
|
|
|
|
|
case 'docx': return 'article';
|
|
|
|
|
case 'xls':
|
|
|
|
|
case 'xlsx': return 'table_chart';
|
|
|
|
|
case 'jpg':
|
|
|
|
|
case 'jpeg':
|
|
|
|
|
case 'png':
|
|
|
|
|
case 'gif': return 'image';
|
|
|
|
|
default: return 'description';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getProgressOffset(): number {
|
|
|
|
|
const circumference = 283; // 2 * PI * 45 (radius)
|
|
|
|
|
return circumference - (this.uploadProgress() / 100) * circumference;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
formatDocType(type: string): string {
|
|
|
|
|
return type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drag & Drop handlers
|
|
|
|
|
onDragOver(event: DragEvent): void {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
this.isDragOver.set(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onDragLeave(event: DragEvent): void {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
this.isDragOver.set(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onDrop(event: DragEvent): void {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
this.isDragOver.set(false);
|
|
|
|
|
const files = event.dataTransfer?.files;
|
|
|
|
|
if (files && files.length > 0) {
|
|
|
|
|
this.selectFile(files[0]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onFileSelected(event: Event): void {
|
|
|
|
|
const input = event.target as HTMLInputElement;
|
|
|
|
|
if (input.files && input.files.length > 0) {
|
|
|
|
|
this.selectFile(input.files[0]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private selectFile(file: File): void {
|
|
|
|
|
// Validate file size
|
|
|
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
|
|
|
this.notification.error('File size must be less than 10MB');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate file type
|
|
|
|
|
const allowedTypes = ['application/pdf', 'image/jpeg', 'image/png', 'image/jpg',
|
|
|
|
|
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
|
|
|
|
|
if (!allowedTypes.includes(file.type)) {
|
|
|
|
|
this.notification.error('Invalid file type. Please upload PDF, JPG, PNG, or DOC files.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.selectedFile.set(file);
|
|
|
|
|
this.uploadState.set('idle');
|
|
|
|
|
this.errorMessage.set('');
|
|
|
|
|
|
|
|
|
|
// Generate preview for images
|
|
|
|
|
if (file.type.startsWith('image/')) {
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onload = (e) => {
|
|
|
|
|
this.previewUrl.set(e.target?.result as string);
|
|
|
|
|
};
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
} else {
|
|
|
|
|
this.previewUrl.set(null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clearFile(event: Event): void {
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
this.selectedFile.set(null);
|
|
|
|
|
this.previewUrl.set(null);
|
|
|
|
|
this.uploadState.set('idle');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resetUpload(): void {
|
|
|
|
|
this.uploadState.set('idle');
|
|
|
|
|
this.uploadProgress.set(0);
|
|
|
|
|
this.errorMessage.set('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
formatFileSize(bytes: number): string {
|
|
|
|
|
if (bytes < 1024) return bytes + ' B';
|
|
|
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
|
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
formatBytes(bytes: number): string {
|
|
|
|
|
return this.formatFileSize(bytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
copyHash(): void {
|
|
|
|
|
const doc = this.uploadedDocument();
|
|
|
|
|
if (doc?.currentHash) {
|
|
|
|
|
this.clipboard.copy(doc.currentHash);
|
|
|
|
|
this.hashCopied.set(true);
|
|
|
|
|
this.notification.success('Hash copied to clipboard');
|
|
|
|
|
setTimeout(() => this.hashCopied.set(false), 2000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onUpload(): void {
|
2026-02-08 02:10:09 -04:00
|
|
|
// Debounce to prevent double-click submissions
|
|
|
|
|
this.submitDebounce(() => this.performUpload());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private performUpload(): void {
|
2026-02-07 10:23:29 -04:00
|
|
|
const file = this.selectedFile();
|
2026-02-08 02:10:09 -04:00
|
|
|
if (!file || this.form.invalid || this.uploadState() === 'uploading') return;
|
2026-02-07 10:23:29 -04:00
|
|
|
|
|
|
|
|
this.uploadState.set('uploading');
|
|
|
|
|
this.uploadProgress.set(0);
|
2026-02-08 02:10:09 -04:00
|
|
|
const rawValues = this.form.getRawValue();
|
|
|
|
|
|
|
|
|
|
// Normalize description
|
|
|
|
|
const docType = rawValues.docType;
|
|
|
|
|
const description = normalizeWhitespace(rawValues.description);
|
2026-02-07 10:23:29 -04:00
|
|
|
|
|
|
|
|
this.documentService.uploadDocumentWithProgress(this.data.requestId, file, docType, description).subscribe({
|
|
|
|
|
next: (progress) => {
|
|
|
|
|
if (progress.complete && progress.response) {
|
|
|
|
|
this.uploadState.set('processing');
|
|
|
|
|
// Simulate brief processing time for blockchain hash generation
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.uploadedDocument.set(progress.response!);
|
|
|
|
|
this.uploadState.set('complete');
|
|
|
|
|
this.notification.success('Document uploaded and hashed successfully');
|
|
|
|
|
}, 800);
|
|
|
|
|
} else {
|
|
|
|
|
this.uploadProgress.set(progress.progress);
|
|
|
|
|
this.uploadedBytes.set(progress.loaded);
|
|
|
|
|
this.totalBytes.set(progress.total);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
error: (err) => {
|
|
|
|
|
this.uploadState.set('error');
|
|
|
|
|
this.errorMessage.set(err.message || 'Failed to upload document. Please try again.');
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onCancel(): void {
|
|
|
|
|
this.dialogRef.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onDone(): void {
|
|
|
|
|
this.dialogRef.close(this.uploadedDocument());
|
|
|
|
|
}
|
|
|
|
|
}
|