Dr-Swopt 1 semana atrás
pai
commit
510cb1f90f

+ 1 - 11
ai-data-entry-ui/src/app/app.html

@@ -1,11 +1 @@
-<div class="app-container">
-  <header>
-    <h1>AI-Assisted Smart Data Entry</h1>
-  </header>
-  
-  <main>
-    <app-claim-form></app-claim-form>
-  </main>
-</div>
-
-<router-outlet />
+<router-outlet></router-outlet>

+ 9 - 1
ai-data-entry-ui/src/app/app.routes.ts

@@ -1,3 +1,11 @@
 import { Routes } from '@angular/router';
+import { UserSelectionComponent } from './components/user-selection/user-selection.component';
+import { DashboardComponent } from './components/claims-dashboard/claims-dashboard.component';
+import { ClaimFormComponent } from './components/claim-form/claim-form.component';
 
-export const routes: Routes = [];
+export const routes: Routes = [
+  { path: '', component: UserSelectionComponent },
+  { path: 'dashboard', component: DashboardComponent },
+  { path: 'extract', component: ClaimFormComponent },
+  { path: '**', redirectTo: '' }
+];

+ 100 - 7
ai-data-entry-ui/src/app/components/claim-form/claim-form.component.css

@@ -1,10 +1,31 @@
-.split-view { 
-    display: flex; 
-    gap: 40px; 
-    padding: 20px; 
-    font-family: 'Inter', sans-serif;
+.form-header {
     max-width: 1200px;
-    margin: 0 auto;
+    margin: 10px auto;
+    padding: 0 20px;
+}
+
+.back-btn {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    background: transparent;
+    border: none;
+    color: #4facfe;
+    cursor: pointer;
+    font-weight: 600;
+    font-size: 0.9rem;
+    padding: 10px 0;
+    transition: color 0.2s;
+}
+
+.back-btn:hover {
+    color: #00f2fe;
+}
+
+.spinner-small {
+    font-size: 0.85rem;
+    color: #4facfe;
+    font-weight: 500;
 }
 
 .receipt-container { 
@@ -30,13 +51,26 @@
     justify-content: center;
     align-items: center;
     overflow: hidden;
+    transition: all 0.3s ease;
+}
+
+.preview-wrapper.dragging {
+    background: rgba(79, 172, 254, 0.1);
+    border-color: #4facfe;
+    transform: scale(1.02);
 }
 
 .receipt-preview { 
     width: 100%; 
     height: auto;
     border-radius: 8px; 
-    box-shadow: 0 4px 6px rgba(0,0,0,0.1); 
+    box-shadow: 0 4px 12px rgba(0,0,0,0.1); 
+    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    cursor: zoom-in;
+}
+
+.receipt-preview:hover {
+    transform: scale(1.1);
 }
 
 .placeholder {
@@ -62,6 +96,27 @@
     z-index: 10;
 }
 
+.review-banner.amber {
+    background: #ffa500;
+}
+
+.error-banner {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    background: #ff4d4f;
+    color: white;
+    padding: 10px;
+    text-align: center;
+    font-size: 0.9rem;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    z-index: 20;
+}
+
 .upload-section {
     margin-top: 20px;
     display: flex;
@@ -147,6 +202,44 @@
     cursor: not-allowed;
 }
 
+.status-badge-container {
+    display: flex;
+    justify-content: center;
+    margin-bottom: 10px;
+}
+
+.status-badge {
+    padding: 6px 12px;
+    border-radius: 20px;
+    font-size: 0.85rem;
+    font-weight: 600;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+
+.status-badge.passed {
+    background: #e6f7ed;
+    color: #28a745;
+    border: 1px solid #28a745;
+}
+
+.status-badge.review {
+    background: #fff4e6;
+    color: #ffa500;
+    border: 1px solid #ffa500;
+}
+
+.warning-text {
+    color: #ffa500;
+    font-weight: bold;
+}
+
+.icon-small {
+    width: 16px;
+    height: 16px;
+}
+
 
 .spinner {
     color: #007bff;

+ 44 - 15
ai-data-entry-ui/src/app/components/claim-form/claim-form.component.html

@@ -1,67 +1,96 @@
+<div class="form-header">
+  <button class="back-btn" routerLink="/dashboard">
+    <lucide-icon [name]="ArrowLeft" class="icon-small"></lucide-icon> Back to Dashboard
+  </button>
+</div>
+
 <div class="split-view">
   <!-- Left Column: Receipt Preview -->
   <div class="receipt-container">
     <h2>Receipt Preview</h2>
-    <div class="preview-wrapper">
+    <div class="preview-wrapper" 
+         [class.dragging]="isDragging"
+         (dragover)="onDragOver($event)" 
+         (dragleave)="onDragLeave($event)" 
+         (drop)="onDrop($event)">
       <img *ngIf="previewUrl" [src]="previewUrl" class="receipt-preview" alt="Receipt Preview">
       <div *ngIf="!previewUrl" class="placeholder">
-        <p>No receipt uploaded. Please select a file to begin extraction.</p>
+        <p>Drag and drop a medical receipt here, or use the button below.</p>
       </div>
       
-      <!-- Overlay for Manual Review -->
-      <div *ngIf="extractionResponse?.needs_manual_review" class="review-banner">
+      <!-- Enhanced Overlay for Manual Review -->
+      <div *ngIf="extractionResponse?.needs_manual_review" class="review-banner amber">
+        <lucide-icon [name]="AlertCircle" class="icon"></lucide-icon>
+        <span>Human Review Advised: Data validation triggered</span>
+      </div>
+
+      <div *ngIf="errorMessage" class="error-banner">
         <lucide-icon [name]="AlertCircle" class="icon"></lucide-icon>
-        <span>Review Required: Blurry or Ambiguous Data</span>
+        <span>{{ errorMessage }}</span>
       </div>
     </div>
     
     <div class="upload-section">
       <input type="file" #fileInput (change)="onFileSelected($event)" style="display: none" accept="image/*">
-      <button class="upload-btn" (click)="fileInput.click()">
-        Upload Medical Receipt
+      <button class="upload-btn" (click)="fileInput.click()" [disabled]="isLoading">
+        {{ previewUrl ? 'Change Receipt' : 'Upload Medical Receipt' }}
       </button>
-      <div *ngIf="isLoading" class="spinner">Extracting data...</div>
+      <div *ngIf="isLoading" class="spinner-small">Extracting data...</div>
     </div>
   </div>
 
   <!-- Right Column: Data Entry Form -->
   <div class="form-container">
     <h2>Claim Information</h2>
-    <form [formGroup]="claimForm">
+    <form [formGroup]="claimForm" (ngSubmit)="onSubmit()">
       
       <!-- Provider Name -->
-      <div class="form-field" [class.ai-unconfident]="confidenceScore < 0.8">
+      <div class="form-field" [class.ai-unconfident]="extractionResponse && extractionResponse.confidence_score < 0.8">
         <label for="provider_name">Provider Name</label>
         <input id="provider_name" type="text" formControlName="provider_name" placeholder="Clinic or Hospital Name">
         <div class="confidence-badge" *ngIf="extractionResponse">
           AI Confidence: {{ (extractionResponse.confidence_score * 100).toFixed(0) }}%
+          <span *ngIf="extractionResponse.confidence_score < 0.8" class="warning-text"> - Low Confidence</span>
         </div>
       </div>
 
       <!-- Visit Date -->
-      <div class="form-field" [class.ai-unconfident]="confidenceScore < 0.8">
+      <div class="form-field" [class.ai-unconfident]="extractionResponse && extractionResponse.confidence_score < 0.8">
         <label for="visit_date">Visit Date</label>
         <input id="visit_date" type="date" formControlName="visit_date">
         <div class="confidence-badge" *ngIf="extractionResponse">
           AI Confidence: {{ (extractionResponse.confidence_score * 100).toFixed(0) }}%
+          <span *ngIf="extractionResponse.confidence_score < 0.8" class="warning-text"> - Low Confidence</span>
         </div>
       </div>
 
       <!-- Total Amount -->
-      <div class="form-field" [class.ai-unconfident]="confidenceScore < 0.8">
+      <div class="form-field" [class.ai-unconfident]="extractionResponse && extractionResponse.confidence_score < 0.8">
         <label for="total_amount">Total Amount</label>
         <div class="amount-input">
           <input id="total_amount" type="number" formControlName="total_amount" step="0.01">
-          <span class="currency-label">MYR</span>
+          <span class="currency-label">{{ claimForm.get('currency')?.value }}</span>
         </div>
         <div class="confidence-badge" *ngIf="extractionResponse">
           AI Confidence: {{ (extractionResponse.confidence_score * 100).toFixed(0) }}%
+          <span *ngIf="extractionResponse.confidence_score < 0.8" class="warning-text"> - Low Confidence</span>
         </div>
       </div>
 
       <div class="form-actions">
-        <button type="submit" [disabled]="!claimForm.valid || isLoading" class="submit-btn">
-          Confirm and Submit Claim
+        <div class="status-badge-container" *ngIf="extractionResponse && !isLoading">
+          <div *ngIf="!extractionResponse.needs_manual_review" class="status-badge passed">
+            <lucide-icon [name]="ShieldCheck" class="icon-small"></lucide-icon>
+            AI Verification Passed
+          </div>
+          <div *ngIf="extractionResponse.needs_manual_review" class="status-badge review">
+            <lucide-icon [name]="AlertCircle" class="icon-small"></lucide-icon>
+            Human Review Advised
+          </div>
+        </div>
+        
+        <button type="submit" [disabled]="!claimForm.valid || isLoading || isSubmitting" class="submit-btn" [class.loading]="isLoading || isSubmitting">
+           {{ isSubmitting ? 'Submitting...' : (isLoading ? 'ProcessingAI...' : 'Confirm and Submit Claim') }}
         </button>
       </div>
     </form>

+ 98 - 26
ai-data-entry-ui/src/app/components/claim-form/claim-form.component.ts

@@ -1,14 +1,16 @@
 import { Component, OnInit } from '@angular/core';
 import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
 import { CommonModule } from '@angular/common';
-import { ExtractionService, ExtractionResponse } from '../../services/extraction.service';
-import { HttpClientModule } from '@angular/common/http';
-import { LucideAngularModule, ShieldCheck, AlertCircle } from 'lucide-angular';
+import { Router, RouterModule } from '@angular/router';
+import { ExtractionService } from '../../services/extraction.service';
+import { ExtractionResponse } from '../../services/extraction';
+import { LucideAngularModule, ShieldCheck, AlertCircle, CheckCircle, ArrowLeft } from 'lucide-angular';
+import { SessionService } from '../../services/session.service';
 
 @Component({
   selector: 'app-claim-form',
   standalone: true,
-  imports: [CommonModule, ReactiveFormsModule, LucideAngularModule],
+  imports: [CommonModule, ReactiveFormsModule, LucideAngularModule, RouterModule],
   templateUrl: './claim-form.component.html',
   styleUrls: ['./claim-form.component.css']
 })
@@ -17,12 +19,19 @@ export class ClaimFormComponent implements OnInit {
   previewUrl: string | null = null;
   extractionResponse: ExtractionResponse | null = null;
   isLoading = false;
+  isSubmitting = false;
+  isDragging = false;
   readonly ShieldCheck = ShieldCheck;
   readonly AlertCircle = AlertCircle;
+  readonly CheckCircle = CheckCircle;
+  readonly ArrowLeft = ArrowLeft;
+  errorMessage: string | null = null;
 
   constructor(
     private fb: FormBuilder,
-    private extractionService: ExtractionService
+    private extractionService: ExtractionService,
+    private sessionService: SessionService,
+    private router: Router
   ) {
     this.claimForm = this.fb.group({
       provider_name: ['', Validators.required],
@@ -32,34 +41,97 @@ export class ClaimFormComponent implements OnInit {
     });
   }
 
-  ngOnInit(): void {}
+  ngOnInit(): void {
+    const user = this.sessionService.getCurrentUser();
+    if (!user) {
+      this.router.navigate(['/']);
+    }
+  }
+
+  onDragOver(event: DragEvent): void {
+    event.preventDefault();
+    event.stopPropagation();
+    this.isDragging = true;
+  }
+
+  onDragLeave(event: DragEvent): void {
+    event.preventDefault();
+    event.stopPropagation();
+    this.isDragging = false;
+  }
+
+  onDrop(event: DragEvent): void {
+    event.preventDefault();
+    event.stopPropagation();
+    this.isDragging = false;
+
+    const files = event.dataTransfer?.files;
+    if (files && files.length > 0) {
+      this.processFile(files[0]);
+    }
+  }
 
   onFileSelected(event: any): void {
     const file: File = event.target.files[0];
     if (file) {
-      this.previewUrl = URL.createObjectURL(file);
-      this.isLoading = true;
-      
-      this.extractionService.extractData(file).subscribe({
-        next: (response) => {
-          this.extractionResponse = response;
-          this.claimForm.patchValue({
-            provider_name: response.provider_name,
-            visit_date: response.visit_date,
-            total_amount: response.total_amount,
-            currency: response.currency
-          });
-          this.isLoading = false;
-        },
-        error: (err) => {
-          console.error('Extraction failed', err);
-          this.isLoading = false;
-          alert('Failed to extract data. Please try again.');
-        }
-      });
+      this.processFile(file);
     }
   }
 
+  private processFile(file: File): void {
+    this.previewUrl = URL.createObjectURL(file);
+    this.isLoading = true;
+    this.errorMessage = null;
+
+    const user = this.sessionService.getCurrentUser();
+
+    this.extractionService.extractData(file, user?.name, user?.department).subscribe({
+      next: (response) => {
+        this.extractionResponse = response;
+        this.claimForm.patchValue({
+          provider_name: response.provider_name,
+          visit_date: response.visit_date,
+          total_amount: response.total_amount,
+          currency: response.currency || 'MYR'
+        });
+        this.isLoading = false;
+      },
+      error: (err: Error) => {
+        console.error('Extraction failed', err);
+        this.errorMessage = err.message;
+        this.isLoading = false;
+      }
+    });
+  }
+
+  onSubmit(): void {
+    if (this.claimForm.invalid) return;
+
+    const user = this.sessionService.getCurrentUser();
+    if (!user) return;
+
+    this.isSubmitting = true;
+    const formData = this.claimForm.value;
+
+    // Merge user-verified data with AI response metadata if needed
+    const submissionData: ExtractionResponse = {
+      ...this.extractionResponse!,
+      ...formData
+    };
+
+    this.extractionService.submitClaim(submissionData, user.name, user.department).subscribe({
+      next: () => {
+        this.isSubmitting = false;
+        this.router.navigate(['/dashboard']);
+      },
+      error: (err) => {
+        console.error('Submission failed', err);
+        this.errorMessage = 'Failed to submit claim. Please try again.';
+        this.isSubmitting = false;
+      }
+    });
+  }
+
   get confidenceScore(): number {
     return this.extractionResponse?.confidence_score ?? 0;
   }

+ 0 - 0
ai-data-entry-ui/src/app/components/claim-form/claim-form.css


+ 0 - 1
ai-data-entry-ui/src/app/components/claim-form/claim-form.html

@@ -1 +0,0 @@
-<p>claim-form works!</p>

+ 0 - 11
ai-data-entry-ui/src/app/components/claim-form/claim-form.ts

@@ -1,11 +0,0 @@
-import { Component } from '@angular/core';
-
-@Component({
-  selector: 'app-claim-form',
-  imports: [],
-  templateUrl: './claim-form.html',
-  styleUrl: './claim-form.css',
-})
-export class ClaimForm {
-
-}

+ 199 - 0
ai-data-entry-ui/src/app/components/claims-dashboard/claims-dashboard.component.css

@@ -0,0 +1,199 @@
+.dashboard-container {
+  padding: 2rem;
+  max-width: 1200px;
+  margin: 0 auto;
+  font-family: 'Inter', sans-serif;
+  color: #fff;
+  background-color: #12121e;
+  min-height: 100vh;
+}
+
+header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 2rem;
+  padding-bottom: 1rem;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+h1 {
+  font-size: 1.8rem;
+  margin: 0;
+  background: linear-gradient(90deg, #00f2fe 0%, #4facfe 100%);
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+}
+
+header p {
+  color: #a0a0c0;
+  margin: 0.25rem 0 0;
+}
+
+.header-right {
+  display: flex;
+  gap: 1rem;
+}
+
+.new-claim-btn {
+  background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
+  color: #fff;
+  border: none;
+  padding: 0.75rem 1.5rem;
+  border-radius: 8px;
+  font-weight: 600;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
+  transition: transform 0.2s;
+}
+
+.new-claim-btn:hover {
+  transform: translateY(-2px);
+}
+
+.logout-btn {
+  background: transparent;
+  color: #a0a0c0;
+  border: 1px solid rgba(160, 160, 192, 0.3);
+  padding: 0.75rem 1.5rem;
+  border-radius: 8px;
+  cursor: pointer;
+}
+
+.logout-btn:hover {
+  background: rgba(255, 255, 255, 0.05);
+  color: #fff;
+}
+
+.stats-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 1.5rem;
+  margin-bottom: 2rem;
+}
+
+.stat-card {
+  background: rgba(255, 255, 255, 0.05);
+  padding: 1.5rem;
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+  gap: 0.5rem;
+}
+
+.stat-label {
+  color: #a0a0c0;
+  font-size: 0.9rem;
+}
+
+.stat-value {
+  font-size: 1.5rem;
+  font-weight: bold;
+}
+
+.status-indicator {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  display: inline-block;
+}
+
+.status-indicator.online {
+  background-color: #00e676;
+  box-shadow: 0 0 10px rgba(0, 230, 118, 0.5);
+}
+
+.table-container {
+  background: rgba(255, 255, 255, 0.03);
+  border-radius: 12px;
+  overflow: hidden;
+  position: relative;
+  min-height: 300px;
+}
+
+table {
+  width: 100%;
+  border-collapse: collapse;
+}
+
+th {
+  text-align: left;
+  padding: 1.25rem 1rem;
+  color: #a0a0c0;
+  font-weight: 500;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+td {
+  padding: 1.25rem 1rem;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+  vertical-align: middle;
+}
+
+.item-tag {
+  background: rgba(79, 172, 254, 0.1);
+  color: #4facfe;
+  padding: 0.2rem 0.6rem;
+  border-radius: 4px;
+  font-size: 0.8rem;
+  margin-right: 0.5rem;
+}
+
+.more-items {
+  color: #a0a0c0;
+  font-size: 0.8rem;
+}
+
+.status-badge {
+  padding: 0.4rem 0.8rem;
+  border-radius: 20px;
+  font-size: 0.85rem;
+  font-weight: 500;
+}
+
+.status-verified {
+  background: rgba(0, 230, 118, 0.1);
+  color: #00e676;
+}
+
+.status-review {
+  background: rgba(255, 171, 0, 0.1);
+  color: #ffab00;
+}
+
+.loading-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: rgba(18, 18, 30, 0.8);
+  z-index: 10;
+}
+
+.spinner {
+  width: 40px;
+  height: 40px;
+  border: 4px solid rgba(79, 172, 254, 0.1);
+  border-top-color: #4facfe;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 1rem;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+.empty-state {
+  text-align: center;
+  padding: 4rem;
+  color: #a0a0c0;
+}

+ 70 - 0
ai-data-entry-ui/src/app/components/claims-dashboard/claims-dashboard.component.html

@@ -0,0 +1,70 @@
+<div class="dashboard-container">
+  <header>
+    <div class="header-left">
+      <h1>Claims Dashboard</h1>
+      <p>Welcome back, <strong>{{ currentUser?.name }}</strong> ({{ currentUser?.department }})</p>
+    </div>
+    <div class="header-right">
+      <button class="new-claim-btn" routerLink="/extract">
+        <span class="plus-icon">+</span> New Claim
+      </button>
+      <button class="logout-btn" (click)="logout()">Logout</button>
+    </div>
+  </header>
+
+  <main>
+    <div class="stats-grid">
+      <div class="stat-card">
+        <span class="stat-label">Total Submissions</span>
+        <span class="stat-value">{{ claims.length }}</span>
+      </div>
+      <div class="stat-card">
+        <span class="stat-label">System Health</span>
+        <span class="status-indicator online"></span>
+        <span class="stat-value">Online</span>
+      </div>
+    </div>
+
+    <div class="table-container">
+      <div *ngIf="loading" class="loading-overlay">
+        <div class="spinner"></div>
+        <p>Fetching claims...</p>
+      </div>
+
+      <table *ngIf="!loading">
+        <thead>
+          <tr>
+            <th>Date</th>
+            <th>Provider</th>
+            <th>Amount</th>
+            <th>Items</th>
+            <th>AI Status</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr *ngFor="let claim of claims">
+            <td>{{ claim.timestamp | date:'mediumDate' }}</td>
+            <td>{{ claim.extraction_data.provider_name }}</td>
+            <td><strong>{{ claim.extraction_data.currency }} {{ claim.extraction_data.total_amount | number:'1.2-2' }}</strong></td>
+            <td>
+              <span class="item-tag" *ngFor="let item of claim.extraction_data.items.slice(0, 2)">
+                {{ item }}
+              </span>
+              <span *ngIf="claim.extraction_data.items.length > 2" class="more-items">
+                +{{ claim.extraction_data.items.length - 2 }}
+              </span>
+            </td>
+            <td>
+              <span class="status-badge" [ngClass]="claim.extraction_data.needs_manual_review ? 'status-review' : 'status-verified'">
+                {{ claim.extraction_data.needs_manual_review ? 'Review Required' : 'Verified' }}
+              </span>
+            </td>
+          </tr>
+          <tr *ngIf="claims.length === 0">
+            <td colspan="5" class="empty-state">No claims found. Start by creating a new one!</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </main>
+</div>

+ 53 - 0
ai-data-entry-ui/src/app/components/claims-dashboard/claims-dashboard.component.ts

@@ -0,0 +1,53 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Router, RouterModule } from '@angular/router';
+import { ExtractionService } from '../../services/extraction.service';
+import { SessionService, User } from '../../services/session.service';
+import { ClaimRecord } from '../../services/extraction';
+
+@Component({
+  selector: 'app-dashboard',
+  standalone: true,
+  imports: [CommonModule, RouterModule],
+  templateUrl: './claims-dashboard.component.html',
+  styleUrls: ['./claims-dashboard.component.css']
+})
+export class DashboardComponent implements OnInit {
+  claims: ClaimRecord[] = [];
+  currentUser: User | null = null;
+  loading = true;
+
+  constructor(
+    private extractionService: ExtractionService,
+    private sessionService: SessionService,
+    private router: Router
+  ) {}
+
+  ngOnInit(): void {
+    this.currentUser = this.sessionService.getCurrentUser();
+    if (!this.currentUser) {
+      this.router.navigate(['/']);
+      return;
+    }
+    this.loadClaims();
+  }
+
+  loadClaims(): void {
+    this.loading = true;
+    this.extractionService.getClaims().subscribe({
+      next: (data) => {
+        this.claims = data.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
+        this.loading = false;
+      },
+      error: (err) => {
+        console.error('Error loading claims:', err);
+        this.loading = false;
+      }
+    });
+  }
+
+  logout(): void {
+    this.sessionService.logout();
+    this.router.navigate(['/']);
+  }
+}

+ 85 - 0
ai-data-entry-ui/src/app/components/user-selection/user-selection.component.css

@@ -0,0 +1,85 @@
+.selection-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100vh;
+  background: linear-gradient(135deg, #1e1e2f 0%, #2a2a40 100%);
+  color: #fff;
+  font-family: 'Inter', sans-serif;
+  text-align: center;
+}
+
+h1 {
+  font-size: 2.5rem;
+  margin-bottom: 0.5rem;
+  background: linear-gradient(90deg, #00f2fe 0%, #4facfe 100%);
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+}
+
+p {
+  color: #a0a0c0;
+  margin-bottom: 3rem;
+}
+
+.user-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 2rem;
+  max-width: 900px;
+  width: 100%;
+  padding: 0 2rem;
+}
+
+.user-card {
+  background: rgba(255, 255, 255, 0.05);
+  border: 1px solid rgba(255, 255, 255, 0.1);
+  border-radius: 16px;
+  padding: 2rem;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  backdrop-filter: blur(10px);
+}
+
+.user-card:hover {
+  transform: translateY(-10px);
+  background: rgba(255, 255, 255, 0.1);
+  border-color: #4facfe;
+  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
+}
+
+.user-avatar {
+  width: 80px;
+  height: 80px;
+  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+  border-radius: 50%;
+  margin: 0 auto 1.5rem;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 2rem;
+  font-weight: bold;
+  color: #fff;
+  box-shadow: 0 4px 15px rgba(79, 172, 254, 0.4);
+}
+
+.user-info h3 {
+  margin: 0;
+  font-size: 1.25rem;
+  color: #fff;
+}
+
+.user-info p {
+  margin: 0.25rem 0 0;
+  font-size: 0.9rem;
+  color: #a0a0c0;
+}
+
+@media (max-width: 768px) {
+  .user-grid {
+    grid-template-columns: 1fr;
+    gap: 1.5rem;
+  }
+}

+ 14 - 0
ai-data-entry-ui/src/app/components/user-selection/user-selection.component.html

@@ -0,0 +1,14 @@
+<div class="selection-container">
+  <h1>Welcome to AI Claims Portal</h1>
+  <p>Please select your profile to continue</p>
+  
+  <div class="user-grid">
+    <div *ngFor="let user of users" class="user-card" (click)="selectUser(user)">
+      <div class="user-avatar">{{ user.name[0] }}</div>
+      <div class="user-info">
+        <h3>{{ user.name }}</h3>
+        <p>{{ user.department }}</p>
+      </div>
+    </div>
+  </div>
+</div>

+ 26 - 0
ai-data-entry-ui/src/app/components/user-selection/user-selection.component.ts

@@ -0,0 +1,26 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Router } from '@angular/router';
+import { SessionService, User } from '../../services/session.service';
+
+@Component({
+  selector: 'app-user-selection',
+  standalone: true,
+  imports: [CommonModule],
+  templateUrl: './user-selection.component.html',
+  styleUrls: ['./user-selection.component.css']
+})
+export class UserSelectionComponent {
+  users: User[] = [
+    { name: 'Ahmad', department: 'Engineering' },
+    { name: 'Siti', department: 'Finance' },
+    { name: 'Tan', department: 'Operations' }
+  ];
+
+  constructor(private sessionService: SessionService, private router: Router) {}
+
+  selectUser(user: User): void {
+    this.sessionService.setCurrentUser(user);
+    this.router.navigate(['/dashboard']);
+  }
+}

+ 43 - 14
ai-data-entry-ui/src/app/services/extraction.service.ts

@@ -1,22 +1,13 @@
 import { Injectable } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
-import { Observable } from 'rxjs';
-
-export interface ExtractionResponse {
-  provider_name: string;
-  visit_date: string;
-  total_amount: number;
-  currency: string;
-  items: string[];
-  confidence_score: number;
-  needs_manual_review: boolean;
-}
+import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
+import { Observable, catchError, throwError } from 'rxjs';
+import { ExtractionResponse, ClaimRecord } from './extraction';
 
 @Injectable({
   providedIn: 'root'
 })
 export class ExtractionService {
-  private apiUrl = 'http://localhost:8000/api/v1/extract';
+  private apiUrl = 'http://localhost:8000/api/v1';
 
   constructor(private http: HttpClient) { }
 
@@ -26,6 +17,44 @@ export class ExtractionService {
     formData.append('user_name', userName);
     formData.append('department', department);
 
-    return this.http.post<ExtractionResponse>(this.apiUrl, formData);
+    return this.http.post<ExtractionResponse>(`${this.apiUrl}/extract`, formData)
+      .pipe(
+        catchError(this.handleError)
+      );
+  }
+
+  submitClaim(extractionData: ExtractionResponse, userName: string, department: string): Observable<ClaimRecord> {
+    const headers = new HttpHeaders({
+      'user-name': userName || 'Unknown',
+      'department': department || 'Unknown'
+    });
+
+    return this.http.post<ClaimRecord>(`${this.apiUrl}/claims`, extractionData, { headers })
+      .pipe(
+        catchError(this.handleError)
+      );
+  }
+
+  getClaims(): Observable<ClaimRecord[]> {
+    return this.http.get<ClaimRecord[]>(`${this.apiUrl}/claims`)
+      .pipe(
+        catchError(this.handleError)
+      );
+  }
+
+  private handleError(error: HttpErrorResponse) {
+    let errorMessage = 'An unknown error occurred!';
+    if (error.error instanceof ErrorEvent) {
+      errorMessage = `Error: ${error.error.message}`;
+    } else {
+      if (error.status === 504 || error.status === 0) {
+        errorMessage = 'The request timed out. Please try again later.';
+      } else if (error.status === 402) {
+        errorMessage = 'AI extraction credits exhausted. Please contact support.';
+      } else {
+        errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
+      }
+    }
+    return throwError(() => new Error(errorMessage));
   }
 }

+ 15 - 6
ai-data-entry-ui/src/app/services/extraction.ts

@@ -1,8 +1,17 @@
-import { Injectable } from '@angular/core';
+export interface ExtractionResponse {
+  provider_name: string;
+  visit_date: string;
+  total_amount: number;
+  currency: string;
+  items: string[];
+  confidence_score: number;
+  needs_manual_review: boolean;
+}
 
-@Injectable({
-  providedIn: 'root',
-})
-export class Extraction {
-  
+export interface ClaimRecord {
+  id: string;
+  timestamp: string;
+  submitted_by: string;
+  department: string;
+  extraction_data: ExtractionResponse;
 }

+ 36 - 0
ai-data-entry-ui/src/app/services/session.service.ts

@@ -0,0 +1,36 @@
+import { Injectable } from '@angular/core';
+import { BehaviorSubject, Observable } from 'rxjs';
+
+export interface User {
+  name: string;
+  department: string;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class SessionService {
+  private currentUserSubject = new BehaviorSubject<User | null>(null);
+  currentUser$: Observable<User | null> = this.currentUserSubject.asObservable();
+
+  constructor() {
+    const savedUser = localStorage.getItem('currentUser');
+    if (savedUser) {
+      this.currentUserSubject.next(JSON.parse(savedUser));
+    }
+  }
+
+  setCurrentUser(user: User): void {
+    localStorage.setItem('currentUser', JSON.stringify(user));
+    this.currentUserSubject.next(user);
+  }
+
+  getCurrentUser(): User | null {
+    return this.currentUserSubject.value;
+  }
+
+  logout(): void {
+    localStorage.removeItem('currentUser');
+    this.currentUserSubject.next(null);
+  }
+}

+ 30 - 6
backend/main.py

@@ -1,9 +1,11 @@
 import os
-from fastapi import FastAPI, UploadFile, File, Header, HTTPException, status
+import uuid
+from datetime import datetime
+from fastapi import FastAPI, UploadFile, File, Header, HTTPException, status, Form
 from fastapi.middleware.cors import CORSMiddleware
-from typing import Optional
+from typing import Optional, List
 from backend.services.openai_service import extract_receipt_data
-from backend.schemas import ExtractionResponse
+from backend.schemas import ExtractionResponse, ClaimRecord
 from dotenv import load_dotenv
 
 load_dotenv()
@@ -19,6 +21,9 @@ app.add_middleware(
     allow_headers=["*"],
 )
 
+# In-memory database for claims
+CLAIMS_DB: List[ClaimRecord] = []
+
 @app.get("/health")
 async def health_check():
     return {"status": "healthy"}
@@ -26,9 +31,8 @@ async def health_check():
 @app.post("/api/v1/extract", response_model=ExtractionResponse)
 async def extract_receipt(
     file: UploadFile = File(...),
-    user_id: Optional[str] = Header(None),
-    user_name: str = "Unknown Employee",
-    department: str = "Unknown Department"
+    user_name: str = Form("Unknown Employee"),
+    department: str = Form("Unknown Department")
 ):
     if not file.content_type.startswith("image/"):
         raise HTTPException(
@@ -53,6 +57,26 @@ async def extract_receipt(
             detail=f"An error occurred during extraction: {str(e)}"
         )
 
+@app.post("/api/v1/claims", response_model=ClaimRecord)
+async def submit_claim(
+    extraction_data: ExtractionResponse,
+    user_name: str = Header(...),
+    department: str = Header(...)
+):
+    claim = ClaimRecord(
+        id=str(uuid.uuid4()),
+        timestamp=datetime.now().isoformat(),
+        submitted_by=user_name,
+        department=department,
+        extraction_data=extraction_data
+    )
+    CLAIMS_DB.append(claim)
+    return claim
+
+@app.get("/api/v1/claims", response_model=List[ClaimRecord])
+async def get_claims():
+    return CLAIMS_DB
+
 if __name__ == "__main__":
     import uvicorn
     uvicorn.run(app, host="0.0.0.0", port=8000)

+ 9 - 0
backend/schemas.py

@@ -1,5 +1,7 @@
 from pydantic import BaseModel, Field
 from typing import List, Optional
+from uuid import UUID
+from datetime import datetime
 
 class ExtractionResponse(BaseModel):
     provider_name: str = Field(description="The name of the clinic or hospital.")
@@ -9,3 +11,10 @@ class ExtractionResponse(BaseModel):
     items: List[str] = Field(description="Simplified list of services (e.g. 'Consultation', 'Medicine').")
     confidence_score: float = Field(description="Model's confidence from 0.0 to 1.0.")
     needs_manual_review: bool = Field(description="Set to true if text is blurry or data is ambiguous.")
+
+class ClaimRecord(BaseModel):
+    id: str = Field(description="Unique identifier for the claim record.")
+    timestamp: str = Field(description="ISO format timestamp of submission.")
+    submitted_by: str = Field(description="Name of the user who submitted the claim.")
+    department: str = Field(description="Department of the user.")
+    extraction_data: ExtractionResponse = Field(description="The AI-extracted data for this claim.")

+ 13 - 5
backend/services/openai_service.py

@@ -44,16 +44,24 @@ async def extract_receipt_data(image_content: bytes, user_name: str, department:
         f"You are an HR data entry assistant helping an employee in Malaysia. "
         f"Extract the requested fields from the provided medical receipt image. "
         f"The employee submitting this is {user_name} from {department}. "
-        f"IMPORTANT: The currency is always Ringgit Malaysia (MYR). Extract the total amount and assume it is in MYR. "
-        f"If the date is missing, look for a 'Payment Date' as a fallback. "
-        f"Analyze the receipt for authenticity. If the total amount appears altered or if the provider name is missing, "
-        f"set `needs_manual_review` to `true` and provide a low `confidence_score`."
+        f"IMPORTANT: The context is Malaysia (MYR). Extract the total amount and assume it is in MYR. "
+        f"If the date is missing, use the 'Transaction Date' or 'Payment Date' as a fallback. "
+        f"Analyze the receipt for authenticity. Set `needs_manual_review` to `true` and provide a low `confidence_score` if: "
+        f"1. The 'Total' does not match the sum of the individual items. "
+        f"2. The receipt looks hand-written and lacks an official stamp. "
+        f"3. The provider name is missing or the amount looks altered. "
+        f"4. The user's name ({user_name}) is not clearly visible on the receipt. "
+        f"If the document is a Tax Invoice, extract the Invoice Number and add it to the `items` list."
     )
     
     # 3. Async Extraction
     completion = await client.beta.chat.completions.parse(
-        model="gpt-4o",
+        model="gpt-4o-mini",
         messages=[
+            {
+                "role": "system",
+                "content": "You are an HR data entry assistant. Extract medical receipt data accurately into structured JSON."
+            },
             {
                 "role": "user",
                 "content": [

+ 2 - 2
requirements.txt

@@ -1,7 +1,7 @@
 fastapi
 uvicorn
-openai
+openai>=1.50.0
 python-dotenv
 python-multipart
-pydantic
+pydantic>=2.0
 Pillow