Mail box and OCR done

This commit is contained in:
2026-01-22 20:56:35 +05:30
parent 2148c4a61c
commit d662aea7e4
16 changed files with 644 additions and 14 deletions

View File

@@ -51,6 +51,12 @@ export class AdminLayoutComponent implements OnInit {
icon: 'pi pi-history'
}
]
},
{
label: 'Mailbox',
icon: 'pi pi-envelope',
routerLink: '/mailbox'
},
{
label: 'Settings',

View File

@@ -2,6 +2,7 @@ import { Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { AdminLayoutComponent } from './admin-layout/admin-layout.component';
import { OcrComponent } from './ocr/ocr.component';
import { MailboxComponent } from './mailbox/mailbox.component';
import { AuthGuard } from './auth.guard';
export const routes: Routes = [
@@ -12,7 +13,8 @@ export const routes: Routes = [
canActivate: [AuthGuard],
children: [
{ path: '', redirectTo: 'ocr', pathMatch: 'full' },
{ path: 'ocr', component: OcrComponent }
{ path: 'ocr', component: OcrComponent },
{ path: 'mailbox', component: MailboxComponent }
]
},
{ path: '**', redirectTo: '' }

View File

@@ -0,0 +1,59 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface AttachmentDTO {
id: number;
filename: string;
content_type: string;
}
export interface EmailDTO {
id: number;
subject: string;
sender: string;
received_date: string;
is_read: boolean;
has_attachments?: boolean; // New flag from backend
body?: string;
attachments?: AttachmentDTO[];
}
export interface EmailListResponse {
items: EmailDTO[];
total: number;
}
@Injectable({
providedIn: 'root'
})
export class MailboxService {
private apiUrl = 'http://localhost:8000/api/emails';
private attachmentUrl = 'http://localhost:8000/api/attachments';
constructor(private http: HttpClient) {}
getEmails(skip: number = 0, limit: number = 50): Observable<EmailListResponse> {
return this.http.get<EmailListResponse>(`${this.apiUrl}?skip=${skip}&limit=${limit}`);
}
getEmailDetails(id: number): Observable<EmailDTO> {
return this.http.get<EmailDTO>(`${this.apiUrl}/${id}`);
}
getAttachmentUrl(id: number): string {
return `${this.attachmentUrl}/${id}`;
}
getPreviewUrl(id: number): string {
return `${this.attachmentUrl}/${id}?view=true`;
}
getZipDownloadUrl(emailId: number): string {
return `${this.apiUrl}/${emailId}/download-all`;
}
syncEmails(): Observable<any> {
return this.http.post<any>(`${this.apiUrl}/sync`, {});
}
}

View File

@@ -0,0 +1,222 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TableModule, TableLazyLoadEvent } from 'primeng/table';
import { ButtonModule } from 'primeng/button';
import { ToastModule } from 'primeng/toast';
import { ProgressBarModule } from 'primeng/progressbar';
import { DialogModule } from 'primeng/dialog';
import { MessageService } from 'primeng/api';
import { MailboxService, EmailDTO } from '../mailbox.service';
@Component({
selector: 'app-mailbox',
standalone: true,
imports: [
CommonModule,
TableModule,
ButtonModule,
ToastModule,
ProgressBarModule,
DialogModule
],
providers: [MessageService],
template: `
<div class="card">
<div class="flex justify-content-between align-items-center mb-4">
<h2>Mailbox</h2>
<button pButton
label="Sync Now"
icon="pi pi-refresh"
(click)="sync()"
[loading]="syncing">
</button>
</div>
<p-progressBar *ngIf="syncing" mode="indeterminate" [style]="{'height': '4px'}"></p-progressBar>
<p-table [value]="emails"
[paginator]="true"
[rows]="50"
[totalRecords]="totalRecords"
[lazy]="true"
(onLazyLoad)="loadEmails($event)"
[loading]="loading"
[showCurrentPageReport]="true"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
styleClass="p-datatable-sm"
[tableStyle]="{'min-width': '50rem'}">
<ng-template pTemplate="header">
<tr>
<th style="width: 5%">ID</th>
<th style="width: 5%"></th> <!-- Attachment Icon -->
<th style="width: 25%">Sender</th>
<th style="width: 40%">Subject</th>
<th style="width: 20%">Date</th>
<th style="width: 5%">Status</th>
<th style="width: 5%">Action</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-email>
<tr [ngClass]="{'font-bold': !email.is_read}">
<td>{{email.id}}</td>
<td>
<i *ngIf="email.has_attachments" class="pi pi-paperclip text-gray-500"></i>
</td>
<td>{{email.sender}}</td>
<td>{{email.subject}}</td>
<td>{{email.received_date | date:'short'}}</td>
<td>
<i class="pi" [ngClass]="email.is_read ? 'pi-envelope-open' : 'pi-envelope'"></i>
</td>
<td>
<button pButton icon="pi pi-eye" class="p-button-rounded p-button-text" (click)="viewEmail(email.id)"></button>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="7">No emails found. Click Sync to fetch from Gmail.</td>
</tr>
</ng-template>
</p-table>
<p-toast></p-toast>
<!-- Email Detail Dialog -->
<p-dialog [(visible)]="displayDialog" [style]="{width: '60vw'}" header="Email Details" [modal]="true">
<div *ngIf="selectedEmail">
<div class="mb-3">
<strong>From:</strong> {{selectedEmail.sender}}<br>
<strong>Subject:</strong> {{selectedEmail.subject}}<br>
<strong>Date:</strong> {{selectedEmail.received_date | date:'medium'}}<br>
</div>
<hr>
<div class="email-body mb-4" [innerHTML]="selectedEmail.body || '<i>No Content</i>'"></div>
<div *ngIf="selectedEmail.attachments && selectedEmail.attachments.length > 0">
<div class="flex justify-content-between align-items-center mb-2">
<h4>Attachments ({{selectedEmail.attachments.length}})</h4>
<a [href]="getDownloadAllLink(selectedEmail.id)" target="_blank" class="p-button p-button-sm p-button-outlined">
<i class="pi pi-download mr-2"></i> Download All
</a>
</div>
<div class="flex flex-wrap gap-3">
<div *ngFor="let att of selectedEmail.attachments" class="attachment-card relative border-1 surface-border border-round p-2 flex flex-column align-items-center justify-content-center" style="width: 120px; height: 120px;">
<!-- Icon Only View -->
<div class="flex align-items-center justify-content-center h-full w-full bg-gray-50 border-round">
<i *ngIf="isImage(att.content_type)" class="pi pi-image text-purple-500 text-5xl"></i>
<i *ngIf="!isImage(att.content_type) && isPdf(att.content_type)" class="pi pi-file-pdf text-red-500 text-5xl"></i>
<i *ngIf="!isImage(att.content_type) && !isPdf(att.content_type)" class="pi pi-file text-gray-500 text-5xl"></i>
</div>
<!-- Hover Overlay -->
<a [href]="getDownloadLink(att.id)" target="_blank" class="download-overlay absolute top-0 left-0 w-full h-full flex align-items-center justify-content-center bg-black-alpha-50 border-round hover:opacity-100 opacity-0 transition-duration-200 cursor-pointer">
<i class="pi pi-download text-white text-3xl"></i>
</a>
<!-- Filename Tooltip/Label (Optional, maybe shortened) -->
<span class="text-xs text-center mt-1 white-space-nowrap overflow-hidden text-overflow-ellipsis w-full" [title]="att.filename">
{{att.filename}}
</span>
</div>
</div>
</div>
</div>
</p-dialog>
</div>
`,
styles: [`
.font-bold { font-weight: 700; }
.email-body { max-height: 400px; overflow-y: auto; background: #f9f9f9; padding: 1rem; border-radius: 4px; }
.attachment-card:hover .download-overlay { opacity: 1; }
.download-overlay { transition: opacity 0.2s; }
`]
})
export class MailboxComponent implements OnInit {
emails: EmailDTO[] = [];
totalRecords: number = 0;
loading: boolean = true;
syncing: boolean = false;
displayDialog: boolean = false;
selectedEmail: EmailDTO | null = null;
constructor(
private mailboxService: MailboxService,
private messageService: MessageService
) {}
ngOnInit() {
// Initial load handled by p-table lazy load
}
loadEmails(event: TableLazyLoadEvent) {
this.loading = true;
const skip = event.first || 0;
const limit = event.rows || 50;
this.mailboxService.getEmails(skip, limit).subscribe({
next: (res) => {
this.emails = res.items;
this.totalRecords = res.total;
this.loading = false;
},
error: (err) => {
console.error(err);
this.loading = false;
}
});
}
viewEmail(id: number) {
this.mailboxService.getEmailDetails(id).subscribe({
next: (email) => {
this.selectedEmail = email;
this.displayDialog = true;
// Update local read status
const idx = this.emails.findIndex(e => e.id === id);
if(idx !== -1) {
this.emails[idx].is_read = true;
}
},
error: (err) => this.messageService.add({severity:'error', summary:'Error', detail:'Could not load email details'})
});
}
getDownloadLink(id: number): string {
return this.mailboxService.getAttachmentUrl(id);
}
getPreviewLink(id: number): string {
return this.mailboxService.getPreviewUrl(id);
}
getDownloadAllLink(emailId: number): string {
return this.mailboxService.getZipDownloadUrl(emailId);
}
isImage(contentType: string): boolean {
return contentType.startsWith('image/');
}
isPdf(contentType: string): boolean {
return contentType === 'application/pdf';
}
sync() {
this.syncing = true;
this.mailboxService.syncEmails().subscribe({
next: (res) => {
this.syncing = false;
this.messageService.add({severity:'success', summary:'Sync Complete', detail: res.message || 'Emails synced successfully'});
// Refresh table
this.loadEmails({first: 0, rows: 50});
},
error: (err) => {
this.syncing = false;
this.messageService.add({severity:'error', summary:'Sync Failed', detail: 'Could not fetch emails'});
}
});
}
}

View File

@@ -35,6 +35,7 @@ import { ToastModule } from 'primeng/toast';
cancelLabel="Clear"
[customUpload]="true"
(uploadHandler)="onUpload($event)"
(onClear)="onClear()"
accept=".pdf,image/*"
maxFileSize="10000000">
</p-fileUpload>
@@ -84,4 +85,8 @@ export class OcrComponent {
}
});
}
onClear() {
this.extractedText = null;
}
}