Bladeren bron

fianl cooked

Dr-Swopt 1 week geleden
bovenliggende
commit
69db225dae

+ 95 - 12
ai-data-entry-ui/src/app/components/claim-form/claim-form.component.css

@@ -97,6 +97,66 @@
     color: #a16207;
 }
 
+.form-section {
+    background: var(--bg-card);
+    padding: 1.5rem;
+    border-radius: 12px;
+    border: 1px solid var(--border-color);
+    margin-bottom: 2rem;
+    box-shadow: var(--shadow-sm);
+}
+
+.section-title {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    font-weight: 700;
+    color: var(--text-primary);
+    margin-bottom: 1.5rem;
+    padding-bottom: 0.75rem;
+    border-bottom: 1px solid var(--border-color);
+    font-size: 1rem;
+}
+
+.auto-filled-badge {
+    background: rgba(34, 197, 94, 0.1);
+    color: #166534;
+    font-size: 0.65rem;
+    padding: 2px 6px;
+    border-radius: 4px;
+    font-weight: 800;
+    text-transform: uppercase;
+    margin-left: 8px;
+    vertical-align: middle;
+}
+
+.submit-btn {
+    width: 100%;
+    padding: 1rem;
+    border-radius: 8px;
+    border: none;
+    background: linear-gradient(90deg, var(--accent-primary) 0%, var(--accent-secondary) 100%);
+    color: #fff;
+    font-weight: 700;
+    cursor: pointer;
+    box-shadow: var(--shadow-sm);
+    transition: transform 0.2s;
+}
+
+.submit-btn:hover:not(:disabled) {
+    transform: translateY(-1px);
+}
+
+.submit-btn:disabled {
+    opacity: 0.6;
+    cursor: not-allowed;
+    background: #cbd5e1;
+}
+
+.internal-section {
+    border-left: 4px solid var(--accent-primary);
+}
+
 .form-field {
     margin-bottom: 1.5rem;
 }
@@ -109,7 +169,8 @@
     font-weight: 500;
 }
 
-.form-field input {
+.form-field input,
+.form-field select {
     width: 100%;
     padding: 0.75rem;
     background: var(--bg-primary);
@@ -118,29 +179,51 @@
     color: var(--text-primary);
     font-size: 1rem;
     transition: border-color 0.2s;
+    font-family: inherit;
 }
 
-.form-field input:focus {
+.form-field select {
+    cursor: pointer;
+}
+
+.form-field input:focus,
+.form-field select:focus {
     border-color: var(--accent-primary);
     outline: none;
     background: #fff;
 }
 
-.submit-btn {
-    width: 100%;
+.declaration-field {
+    margin-top: 1.5rem;
     padding: 1rem;
+    background: #f8fafc;
     border-radius: 8px;
-    border: none;
-    background: linear-gradient(90deg, var(--accent-primary) 0%, var(--accent-secondary) 100%);
-    color: #fff;
-    font-weight: 700;
+    border: 1px solid var(--border-color);
+}
+
+.checkbox-container {
+    display: flex;
+    align-items: flex-start;
+    gap: 12px;
     cursor: pointer;
-    box-shadow: var(--shadow-sm);
-    transition: transform 0.2s;
+    font-size: 0.9rem;
+    color: var(--text-secondary);
+    line-height: 1.4;
 }
 
-.submit-btn:hover {
-    transform: translateY(-1px);
+.checkbox-container input {
+    width: 18px;
+    height: 18px;
+    margin-top: 2px;
+    cursor: pointer;
+}
+
+.error-text {
+    color: #ef4444;
+    font-size: 0.85rem;
+    margin-top: 1rem;
+    text-align: center;
+    font-weight: 500;
 }
 
 .transparency-section {

+ 76 - 6
ai-data-entry-ui/src/app/components/claim-form/claim-form.component.html

@@ -71,9 +71,14 @@
 
     <form [formGroup]="claimForm" (ngSubmit)="onSubmit()">
       <div class="form-section">
+        <div class="section-title">
+          <lucide-icon [name]="ShieldCheck" class="icon-small"></lucide-icon>
+          Basic & Financial Information
+        </div>
+        
         <div class="financial-row">
           <div class="form-field">
-            <label>Amount Spent (MYR)</label>
+            <label>Amount Spent (MYR) <span class="auto-filled-badge" *ngIf="isAutoFilled('amount_spent')">AI</span></label>
             <input type="number" formControlName="amount_spent" step="0.01" placeholder="E.g. 150.00">
           </div>
           <div class="form-field">
@@ -84,20 +89,85 @@
           </div>
         </div>
 
-        <div class="form-field" style="margin-top: 1rem;">
-          <label>Provider Name</label>
+        <div class="form-field">
+          <label>Provider Name <span class="auto-filled-badge" *ngIf="isAutoFilled('provider_name')">AI</span></label>
           <input type="text" formControlName="provider_name" placeholder="E.g. Klinik Kesihatan">
         </div>
+        
+        <div class="financial-row">
+          <div class="form-field">
+            <label>Visit Date <span class="auto-filled-badge" *ngIf="isAutoFilled('visit_date')">AI</span></label>
+            <input type="date" formControlName="visit_date">
+          </div>
+          <div class="form-field">
+             <label>Claim Category <span class="auto-filled-badge" *ngIf="isAutoFilled('claim_category')">AI</span></label>
+             <select formControlName="claim_category">
+               <option value="" disabled selected>Select Category</option>
+               <option *ngFor="let cat of categories" [value]="cat">{{ cat }}</option>
+             </select>
+          </div>
+        </div>
+      </div>
+
+      <!-- AI-Assisted Auditor Fields -->
+      <div class="form-section">
+        <div class="section-title">
+          <lucide-icon [name]="CheckCircle" class="icon-small"></lucide-icon>
+          Audit & Verification Details
+        </div>
+        
+        <div class="financial-row">
+          <div class="form-field">
+            <label>Receipt Reference No <span class="auto-filled-badge" *ngIf="isAutoFilled('receipt_ref_no')">AI</span></label>
+            <input type="text" formControlName="receipt_ref_no" placeholder="Inv-12345">
+          </div>
+          <div class="form-field">
+            <label>Clinic/Hospital Reg No <span class="auto-filled-badge" *ngIf="isAutoFilled('clinic_reg_no')">AI</span></label>
+            <input type="text" formControlName="clinic_reg_no" placeholder="E.g. 123456-X">
+          </div>
+        </div>
+
         <div class="form-field">
-          <label>Visit Date</label>
-          <input type="date" formControlName="visit_date">
+           <label>Diagnosis / Items Brief <span class="auto-filled-badge" *ngIf="isAutoFilled('diagnosis_brief')">AI</span></label>
+           <input type="text" formControlName="diagnosis_brief" placeholder="E.g. Consultation and Flu medication">
+        </div>
+      </div>
+
+      <!-- MANDATORY INTERNAL DETAILS -->
+      <div class="form-section internal-section">
+        <div class="section-title">
+          <lucide-icon [name]="ClipboardList" class="icon-small"></lucide-icon>
+          Internal Mandatory Details (Requires Manual Input)
+        </div>
+
+        <div class="financial-row">
+          <div class="form-field">
+            <label>Treatment Type</label>
+            <select formControlName="treatment_type">
+              <option value="" disabled selected>Select Treatment Type</option>
+              <option *ngFor="let type of treatmentTypes" [value]="type">{{ type }}</option>
+            </select>
+          </div>
+          <div class="form-field">
+             <label>Cost Center Code</label>
+             <input type="text" formControlName="cost_center" placeholder="E.g. FIN-001">
+          </div>
+        </div>
+
+        <div class="declaration-field">
+           <label class="checkbox-container">
+             <input type="checkbox" formControlName="declaration_signed">
+             <span class="checkmark"></span>
+             I hereby declare that the information provided is true and the expenses were incurred for official medical purposes.
+           </label>
         </div>
       </div>
 
       <div class="form-actions">
-        <button type="submit" [disabled]="!claimForm.valid || isLoading || isSubmitting" class="submit-btn" [class.loading]="isLoading || isSubmitting">
+        <button type="submit" [disabled]="claimForm.invalid || isLoading || isSubmitting" class="submit-btn" [class.loading]="isLoading || isSubmitting">
            {{ isSubmitting ? 'Finalizing Claim...' : 'Submit Official Claim' }}
         </button>
+        <p class="error-text" *ngIf="claimForm.invalid && claimForm.touched">Please complete all required fields and sign the declaration.</p>
       </div>
     </form>
 

+ 53 - 6
ai-data-entry-ui/src/app/components/claim-form/claim-form.component.ts

@@ -5,7 +5,7 @@ import { Router, RouterModule } from '@angular/router';
 import { ExtractionService } from '../../services/extraction.service';
 import { SessionService, User } from '../../services/session.service';
 import { ExtractionResponse, ClaimSubmission } from '../../services/extraction';
-import { LucideAngularModule, ShieldCheck, AlertCircle, CheckCircle, ArrowLeft, Eye, EyeOff, Wand2 } from 'lucide-angular';
+import { LucideAngularModule, ShieldCheck, AlertCircle, CheckCircle, ArrowLeft, Eye, EyeOff, Wand2, ClipboardList } from 'lucide-angular';
 import { Subscription } from 'rxjs';
 
 @Component({
@@ -35,8 +35,15 @@ export class ClaimFormComponent implements OnInit, OnDestroy {
   readonly Eye = Eye;
   readonly EyeOff = EyeOff;
   readonly Wand2 = Wand2;
+  readonly ClipboardList = ClipboardList;
   errorMessage: string | null = null;
 
+  // Track which fields were auto-filled by AI
+  autoFilledFields: Set<string> = new Set();
+
+  categories = ['General', 'Dental', 'Optical', 'Specialist'];
+  treatmentTypes = ['Outpatient', 'Inpatient', 'Dental Care', 'Optical Care', 'Health Screening'];
+
   constructor(
     private fb: FormBuilder,
     private extractionService: ExtractionService,
@@ -48,7 +55,16 @@ export class ClaimFormComponent implements OnInit, OnDestroy {
       visit_date: ['', Validators.required],
       amount_spent: ['', [Validators.required, Validators.min(0.01)]],
       amount_claimed: [{ value: 0, disabled: true }],
-      currency: ['MYR']
+      currency: ['MYR'],
+      // AI-assisted fields
+      receipt_ref_no: ['', Validators.required],
+      clinic_reg_no: ['', Validators.required],
+      claim_category: ['', Validators.required],
+      diagnosis_brief: ['', Validators.required],
+      // Mandatory Manual fields
+      treatment_type: ['', Validators.required],
+      cost_center: ['', Validators.required],
+      declaration_signed: [false, Validators.requiredTrue]
     });
   }
 
@@ -114,8 +130,9 @@ export class ClaimFormComponent implements OnInit, OnDestroy {
     if (!this.currentUser) return;
     this.selectedFile = file;
     this.previewUrl = URL.createObjectURL(file);
-    this.extractionResponse = null; // reset if existing
+    this.extractionResponse = null; 
     this.errorMessage = null;
+    this.autoFilledFields.clear();
   }
 
   autoFillWithAI(): void {
@@ -123,17 +140,40 @@ export class ClaimFormComponent implements OnInit, OnDestroy {
 
     this.isLoading = true;
     this.errorMessage = null;
+    this.autoFilledFields.clear();
     
     this.extractionService.extractData(this.selectedFile, this.currentUser.name, this.currentUser.department).subscribe({
       next: (response) => {
         this.extractionResponse = response;
-        this.claimForm.patchValue({
+        
+        const patchData: any = {
           provider_name: response.provider_name,
           visit_date: response.visit_date,
           amount_spent: response.total_amount,
           currency: response.currency || 'MYR'
-        });
-        // The valueChanges subscription will trigger calculateClaimable automatically
+        };
+
+        // Track auto-filled fields for UI cues
+        ['provider_name', 'visit_date', 'amount_spent'].forEach(f => this.autoFilledFields.add(f));
+
+        if (response.receipt_ref_no) {
+          patchData.receipt_ref_no = response.receipt_ref_no;
+          this.autoFilledFields.add('receipt_ref_no');
+        }
+        if (response.clinic_reg_no) {
+          patchData.clinic_reg_no = response.clinic_reg_no;
+          this.autoFilledFields.add('clinic_reg_no');
+        }
+        if (response.claim_category) {
+          patchData.claim_category = response.claim_category;
+          this.autoFilledFields.add('claim_category');
+        }
+        if (response.diagnosis_brief) {
+          patchData.diagnosis_brief = response.diagnosis_brief;
+          this.autoFilledFields.add('diagnosis_brief');
+        }
+
+        this.claimForm.patchValue(patchData);
         this.isLoading = false;
       },
       error: (err: Error) => {
@@ -155,6 +195,9 @@ export class ClaimFormComponent implements OnInit, OnDestroy {
       visit_date: formData.visit_date,
       amount_spent: Number(formData.amount_spent),
       currency: formData.currency,
+      treatment_type: formData.treatment_type,
+      cost_center: formData.cost_center,
+      declaration_signed: formData.declaration_signed,
       extraction_data: this.extractionResponse
     };
 
@@ -171,6 +214,10 @@ export class ClaimFormComponent implements OnInit, OnDestroy {
     });
   }
 
+  isAutoFilled(fieldName: string): boolean {
+    return this.autoFilledFields.has(fieldName);
+  }
+
   get rawJson(): string {
     return JSON.stringify(this.extractionResponse, null, 2);
   }

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

@@ -293,6 +293,13 @@ h1 { font-size: 2rem; margin: 0; color: var(--text-primary); }
 
 .audit-item .label { color: var(--text-secondary); }
 
+.audit-divider {
+    height: 1px;
+    background: var(--border-color);
+    margin: 1.5rem 0;
+    opacity: 0.6;
+}
+
 .audit-item.total {
     margin-top: 1.5rem;
     padding-top: 1.5rem;

+ 32 - 1
ai-data-entry-ui/src/app/components/claims-dashboard/claims-dashboard.component.html

@@ -139,7 +139,38 @@
           </div>
 
           <div class="financial-audit">
-            <h3>Policy Enforcement</h3>
+            <h3>Claim Metadata & Policy</h3>
+            
+            <div class="audit-item">
+              <span class="label">Benefit Category:</span>
+              <span class="val">{{ selectedClaim.extraction_data?.claim_category || 'General' }}</span>
+            </div>
+            <div class="audit-item">
+              <span class="label">Treatment Type:</span>
+              <span class="val">{{ selectedClaim.treatment_type }}</span>
+            </div>
+            <div class="audit-item">
+              <span class="label">Cost Center:</span>
+              <span class="val">{{ selectedClaim.cost_center }}</span>
+            </div>
+
+            <div class="audit-divider"></div>
+
+            <div class="audit-item">
+              <span class="label">Receipt Ref:</span>
+              <span class="val">{{ selectedClaim.extraction_data?.receipt_ref_no || 'N/A' }}</span>
+            </div>
+            <div class="audit-item">
+              <span class="label">Clinic Reg No:</span>
+              <span class="val">{{ selectedClaim.extraction_data?.clinic_reg_no || 'N/A' }}</span>
+            </div>
+            <div class="audit-item">
+              <span class="label">Diagnosis:</span>
+              <span class="val" style="text-align: right; max-width: 60%;">{{ selectedClaim.extraction_data?.diagnosis_brief || 'Manual Entry' }}</span>
+            </div>
+
+            <div class="audit-divider"></div>
+
             <div class="audit-item">
               <span class="label">Gross Spent:</span>
               <span class="val">RM {{ selectedClaim.amount_spent | number:'1.2-2' }}</span>

+ 12 - 2
ai-data-entry-ui/src/app/services/extraction.ts

@@ -14,6 +14,10 @@ export interface ExtractionResponse {
   confidence_score: number;
   needs_manual_review: boolean;
   ai_reasoning: string;
+  receipt_ref_no?: string | null;
+  clinic_reg_no?: string | null;
+  claim_category?: string | null;
+  diagnosis_brief?: string | null;
 }
 
 export interface ClaimSubmission {
@@ -21,6 +25,9 @@ export interface ClaimSubmission {
   visit_date: string;
   amount_spent: number;
   currency: string;
+  treatment_type: string;
+  cost_center: string;
+  declaration_signed: boolean;
   extraction_data?: ExtractionResponse | null;
 }
 
@@ -31,7 +38,10 @@ export interface ClaimRecord {
   department: string;
   amount_spent: number;
   amount_claimed: number;
-  provider_name?: string;
-  visit_date?: string;
+  provider_name: string;
+  visit_date: string;
+  treatment_type: string;
+  cost_center: string;
+  declaration_signed: boolean;
   extraction_data?: ExtractionResponse | null;
 }

+ 3 - 0
backend/main.py

@@ -95,6 +95,9 @@ async def submit_claim(
         amount_claimed=amount_claimed,
         provider_name=submission_data.provider_name,
         visit_date=submission_data.visit_date,
+        treatment_type=submission_data.treatment_type,
+        cost_center=submission_data.cost_center,
+        declaration_signed=submission_data.declaration_signed,
         extraction_data=submission_data.extraction_data
     )
     CLAIMS_DB.append(claim)

+ 10 - 0
backend/schemas.py

@@ -18,12 +18,19 @@ class ExtractionResponse(BaseModel):
     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.")
     ai_reasoning: str = Field(description="A brief explanation of how the data was identified.")
+    receipt_ref_no: Optional[str] = Field(default=None, description="The reference number or invoice number on the receipt.")
+    clinic_reg_no: Optional[str] = Field(default=None, description="The clinic's official registration number (SSM/MOH).")
+    claim_category: Optional[str] = Field(default=None, description="Category: General, Dental, Optical, Specialist.")
+    diagnosis_brief: Optional[str] = Field(default=None, description="A very short summary of the diagnosis or items seen.")
 
 class ClaimSubmission(BaseModel):
     provider_name: str = Field(description="The name of the clinic or hospital.")
     visit_date: str = Field(description="The date of service in YYYY-MM-DD format.")
     amount_spent: float = Field(description="The manual or validated total amount spent.")
     currency: str = Field(description="3-letter currency code (e.g. USD, MYR).")
+    treatment_type: str = Field(description="Type of treatment (Outpatient, Dental, etc.).")
+    cost_center: str = Field(description="Internal cost center code.")
+    declaration_signed: bool = Field(description="Whether the user signed the compliance declaration.")
     extraction_data: Optional[ExtractionResponse] = Field(default=None, description="Optional AI extraction data if used.")
 
 class ClaimRecord(BaseModel):
@@ -35,4 +42,7 @@ class ClaimRecord(BaseModel):
     amount_claimed: float = Field(description="The actual amount credited after policy capping.")
     provider_name: str = Field(description="The name of the clinic or hospital.")
     visit_date: str = Field(description="The date of service in YYYY-MM-DD format.")
+    treatment_type: str = Field(description="Type of treatment.")
+    cost_center: str = Field(description="Internal cost center code.")
+    declaration_signed: bool = Field(description="Whether the user signed the compliance declaration.")
     extraction_data: Optional[ExtractionResponse] = Field(default=None, description="The optional AI-extracted data for this claim.")

+ 7 - 6
backend/services/openai_service.py

@@ -41,18 +41,19 @@ async def extract_receipt_data(image_content: bytes, user_name: str, department:
     
     # 2. Refined Prompt
     prompt = (
-        f"You are an HR data entry assistant helping an employee in Malaysia. "
+        f"You are a cautious auditor helping an HR department 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 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"IMPORTANT: The context is Malaysia (MYR). "
+        f"For the fields `receipt_ref_no` and `clinic_reg_no`, only provide a value if you can read it clearly without any guessing or inference. If the text is smudged, handwritten, or ambiguous, return `null`. "
+        f"Map the clinic/services to a `claim_category` from: [General, Dental, Optical, Specialist] based on the clinic name or invoice items. "
+        f"Provide a 1-sentence `diagnosis_brief` summarizing the services seen (e.g. 'Fever consultation and medicine'). "
+        f"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"IMPORTANT: Fill the `ai_reasoning` field with a 2-sentence explanation of how you identified the clinic and the total amount. "
-        f"If the document is a Tax Invoice, extract the Invoice Number and add it to the `items` list."
+        f"IMPORTANT: Fill the `ai_reasoning` field with a 1-sentence explanation of how you identified the clinic and category."
     )
     
     # 3. Async Extraction