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';
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: `
@if (uploadState() === 'complete' && uploadedDocument()) {
check_circle
Document Uploaded Successfully
Your document has been securely uploaded and hashed on the blockchain.
@if (isImageFile()) {
![Document preview]()
} @else {
{{ getFileIcon() }}
}
{{ uploadedDocument()!.originalFilename }}
folder
{{ formatDocType(uploadedDocument()!.docType) }}
schedule
Just now
{{ uploadedDocument()!.currentHash }}
This cryptographic hash uniquely identifies your document and is permanently recorded on the blockchain.
} @else {
}
@if (uploadState() === 'complete') {
} @else {
}
`,
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);
private readonly data: DocumentUploadDialogData = inject(MAT_DIALOG_DATA);
private readonly clipboard = inject(Clipboard);
// State signals
readonly uploadState = signal('idle');
readonly selectedFile = signal(null);
readonly isDragOver = signal(false);
readonly uploadProgress = signal(0);
readonly uploadedBytes = signal(0);
readonly totalBytes = signal(0);
readonly uploadedDocument = signal(null);
readonly errorMessage = signal('');
readonly previewUrl = signal(null);
readonly hashCopied = signal(false);
readonly documentTypes: { value: DocumentType; label: string; icon: string }[] = [
{ value: 'FIRE_SAFETY_CERTIFICATE', label: 'Fire Safety Certificate', icon: 'local_fire_department' },
{ value: 'BUILDING_PLAN', label: 'Building Plan', icon: 'apartment' },
{ value: 'PROPERTY_OWNERSHIP', label: 'Property Ownership', icon: 'home' },
{ value: 'INSPECTION_REPORT', label: 'Inspection Report', icon: 'fact_check' },
{ value: 'POLLUTION_CERTIFICATE', label: 'Pollution Certificate', icon: 'eco' },
{ value: 'ELECTRICAL_SAFETY_CERTIFICATE', label: 'Electrical Safety Certificate', icon: 'electrical_services' },
{ value: 'STRUCTURAL_STABILITY_CERTIFICATE', label: 'Structural Stability Certificate', icon: 'foundation' },
{ value: 'IDENTITY_PROOF', label: 'Identity Proof', icon: 'badge' },
{ value: 'ADDRESS_PROOF', label: 'Address Proof', icon: 'location_on' },
{ value: 'OTHER', label: 'Other Document', icon: 'description' },
];
readonly form = this.fb.nonNullable.group({
docType: ['' as DocumentType, [Validators.required]],
description: [''],
});
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 {
const file = this.selectedFile();
if (!file || this.form.invalid) return;
this.uploadState.set('uploading');
this.uploadProgress.set(0);
const { docType, description } = this.form.getRawValue();
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());
}
}