# LIS Result Report Printing — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Generate professional PDF result reports with lab letterhead, patient info, results table, doctor signature, QR verification code — printable per-request or per-section, with a batch print queue screen.

**Architecture:** A new `LisReportPdfService` handles all PDF generation using jsPDF + jspdf-autotable. Report settings are stored via existing `lis_settings` API. A new Print Queue component at `/lab/print-queue` enables batch printing. Print buttons are added to Results page and Requests list. QR codes use the `qrcode` npm package.

**Tech Stack:** jsPDF 4.2, jspdf-autotable 5.0, JsBarcode 3.12, qrcode (new), Angular 21 standalone components, PrimeNG 21, signals.

---

### Task 1: Install QR Code Package

**Files:**
- Modify: `package.json`

**Step 1: Install qrcode package**

```bash
cd /home/moonui/public_html/moon-erp && npm install qrcode @types/qrcode --save
```

Expected: Package installed, no errors.

---

### Task 2: Add Report Settings to LIS Settings API

**Files:**
- Modify: `src/app/features/lis/lis-layout/lis-layout.component.ts` (check existing settings route)

**Step 1: Add report settings via API**

Use curl to save default report settings:

```bash
TOKEN=$(curl -s -X POST https://moon-erp.elbaset.com/api/auth/login \
  -H 'Accept: application/json' -H 'Content-Type: application/json' \
  -d '{"email":"hazem@gt4it.com","password":"123456789"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")

# Save each setting
for key_val in \
  "lis_report_mode:per_request" \
  "lis_report_header_text:" \
  "lis_report_footer_text:Results are for the requesting physician only and are not a diagnosis." \
  "lis_report_license_number:" \
  "lis_report_show_qr:1" \
  "lis_report_show_signature:1" \
  "lis_report_show_previous:0" \
  "lis_label_auto_print:0" \
  "lis_label_copies:1"; do
  KEY="${key_val%%:*}"
  VAL="${key_val#*:}"
  curl -s -X POST "https://moon-erp.elbaset.com/api/core/settings" \
    -H "X-Authorization: Bearer $TOKEN" \
    -H 'Accept: application/json' \
    -H 'Content-Type: application/json' \
    -d "{\"key\":\"$KEY\",\"value\":\"$VAL\"}"
  echo " → $KEY"
done
```

---

### Task 3: Create Report PDF Service

**Files:**
- Create: `src/app/core/services/lis-report-pdf.service.ts`

**Step 1: Create the PDF generation service**

This service generates the full result report PDF. Key responsibilities:
- Load company info (logo, name, address)
- Build header with lab info
- Build patient info section
- Build results table grouped by section
- Add doctor signature (image or text)
- Add QR verification code
- Add footer disclaimer
- Support per-request and per-section modes

```typescript
import { Injectable, inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
import * as QRCode from 'qrcode';
import { LanguageService } from './language.service';

export interface ReportData {
  requestNumber: string;
  requestDate: string;
  priority: string;
  patient: {
    name: string;
    mrn: string;
    age: number | null;
    gender: string;
    phone?: string;
  };
  doctor: {
    name: string;
    title?: string;
    signatureUrl?: string;
  } | null;
  sections: ReportSection[];
}

export interface ReportSection {
  name: string;
  results: ReportResultRow[];
}

export interface ReportResultRow {
  testName: string;
  testCode: string;
  result: string;
  unit: string;
  referenceRange: string;
  flag: string;
  previousResult?: string;
}

export interface ReportSettings {
  report_mode: 'per_request' | 'per_section';
  report_header_text: string;
  report_footer_text: string;
  report_license_number: string;
  report_show_qr: boolean;
  report_show_signature: boolean;
  report_show_previous: boolean;
}

export interface LabInfo {
  name: string;
  nameEn: string;
  address: string;
  phone: string;
  email: string;
  logoUrl: string | null;
}

@Injectable({ providedIn: 'root' })
export class LisReportPdfService {
  private translate = inject(TranslateService);
  private lang = inject(LanguageService);

  private readonly PAGE_WIDTH = 210; // A4 mm
  private readonly PAGE_HEIGHT = 297;
  private readonly MARGIN = 15;
  private readonly CONTENT_WIDTH = 180; // 210 - 2*15

  async generateReport(
    data: ReportData,
    settings: ReportSettings,
    lab: LabInfo,
    logoBase64?: string
  ): Promise<jsPDF> {
    const isRtl = this.lang.currentLang() === 'ar';
    const doc = new jsPDF({ unit: 'mm', format: 'a4' });

    let y = this.MARGIN;

    // ─── Header: Logo + Lab Info ───
    y = this.drawHeader(doc, lab, logoBase64, settings, y, isRtl);

    // ─── Separator ───
    doc.setDrawColor(0, 150, 136); // teal accent
    doc.setLineWidth(0.5);
    doc.line(this.MARGIN, y, this.PAGE_WIDTH - this.MARGIN, y);
    y += 4;

    // ─── Patient Info ───
    y = this.drawPatientInfo(doc, data, y, isRtl);

    // ─── Separator ───
    doc.setDrawColor(200);
    doc.setLineWidth(0.2);
    doc.line(this.MARGIN, y, this.PAGE_WIDTH - this.MARGIN, y);
    y += 4;

    // ─── Results Tables by Section ───
    for (const section of data.sections) {
      // Check if we need a new page
      if (y > this.PAGE_HEIGHT - 60) {
        doc.addPage();
        y = this.MARGIN;
      }

      // Section header
      doc.setFontSize(11);
      doc.setFont('helvetica', 'bold');
      doc.setTextColor(0, 150, 136);
      const sectionX = isRtl ? this.PAGE_WIDTH - this.MARGIN : this.MARGIN;
      doc.text(section.name, sectionX, y, { align: isRtl ? 'right' : 'left' });
      y += 5;

      // Results table
      const headers = [
        this.translate.instant('LIS.RESULTS.INVESTIGATION'),
        this.translate.instant('LIS.RESULTS.VALUE'),
        this.translate.instant('LIS.INVESTIGATIONS.UNIT'),
        this.translate.instant('LIS.INVESTIGATIONS.REFERENCE_RANGE'),
        this.translate.instant('LIS.RESULTS.FLAG'),
      ];

      if (settings.report_show_previous) {
        headers.push(this.translate.instant('LIS.PRINT_QUEUE.PREVIOUS'));
      }

      const rows = section.results.map((r) => {
        const row = [r.testName, r.result, r.unit, r.referenceRange, r.flag];
        if (settings.report_show_previous) {
          row.push(r.previousResult || '-');
        }
        return row;
      });

      autoTable(doc, {
        startY: y,
        head: [headers],
        body: rows,
        margin: { left: this.MARGIN, right: this.MARGIN },
        styles: {
          fontSize: 9,
          cellPadding: 2,
          lineColor: [200, 200, 200],
          lineWidth: 0.1,
        },
        headStyles: {
          fillColor: [0, 150, 136],
          textColor: 255,
          fontStyle: 'bold',
          fontSize: 9,
        },
        bodyStyles: {
          textColor: [30, 41, 59],
        },
        didParseCell: (data: any) => {
          // Highlight abnormal flags in red
          if (data.section === 'body' && data.column.index === 4) {
            const flag = data.cell.raw;
            if (flag && flag !== 'normal' && flag !== '-' && flag !== '') {
              data.cell.styles.textColor = [220, 38, 38];
              data.cell.styles.fontStyle = 'bold';
            }
          }
          // Bold the result value column
          if (data.section === 'body' && data.column.index === 1) {
            data.cell.styles.fontStyle = 'bold';
          }
        },
        columnStyles: isRtl
          ? { 0: { halign: 'right' }, 1: { halign: 'center' }, 2: { halign: 'center' }, 3: { halign: 'center' }, 4: { halign: 'center' } }
          : {},
      });

      y = (doc as any).lastAutoTable.finalY + 6;
    }

    // ─── Footer Section: Signature + QR ───
    if (y > this.PAGE_HEIGHT - 50) {
      doc.addPage();
      y = this.MARGIN;
    }

    // Separator
    doc.setDrawColor(200);
    doc.setLineWidth(0.2);
    doc.line(this.MARGIN, y, this.PAGE_WIDTH - this.MARGIN, y);
    y += 6;

    // Signature and QR side by side
    const signatureX = isRtl ? this.PAGE_WIDTH - this.MARGIN - 60 : this.MARGIN;
    const qrX = isRtl ? this.MARGIN : this.PAGE_WIDTH - this.MARGIN - 30;

    // Doctor signature
    if (settings.report_show_signature && data.doctor) {
      if (data.doctor.signatureUrl) {
        try {
          doc.addImage(data.doctor.signatureUrl, 'PNG', signatureX, y, 50, 20);
          y += 22;
        } catch {
          // Fallback to text
        }
      }
      doc.setFontSize(10);
      doc.setFont('helvetica', 'bold');
      doc.setTextColor(30, 41, 59);
      doc.text(data.doctor.name, signatureX, y);
      if (data.doctor.title) {
        doc.setFontSize(8);
        doc.setFont('helvetica', 'normal');
        doc.text(data.doctor.title, signatureX, y + 4);
      }
    }

    // QR Code
    if (settings.report_show_qr) {
      try {
        const verifyUrl = `https://moonui.elbaset.com/app/verify/${data.requestNumber}`;
        const qrDataUrl = await QRCode.toDataURL(verifyUrl, { width: 100, margin: 1 });
        doc.addImage(qrDataUrl, 'PNG', qrX, y - 6, 28, 28);
        doc.setFontSize(6);
        doc.setTextColor(100);
        doc.text(this.translate.instant('LIS.PRINT_QUEUE.SCAN_TO_VERIFY'), qrX, y + 24, { maxWidth: 28 });
      } catch {}
    }

    y += 30;

    // Footer disclaimer
    if (settings.report_footer_text) {
      doc.setFontSize(7);
      doc.setTextColor(100, 116, 139);
      doc.setFont('helvetica', 'italic');
      const footerX = isRtl ? this.PAGE_WIDTH - this.MARGIN : this.MARGIN;
      doc.text(settings.report_footer_text, footerX, this.PAGE_HEIGHT - 10, {
        align: isRtl ? 'right' : 'left',
        maxWidth: this.CONTENT_WIDTH,
      });
    }

    // Page numbers
    const pageCount = doc.getNumberOfPages();
    for (let i = 1; i <= pageCount; i++) {
      doc.setPage(i);
      doc.setFontSize(7);
      doc.setTextColor(150);
      doc.text(
        `${i} / ${pageCount}`,
        this.PAGE_WIDTH / 2,
        this.PAGE_HEIGHT - 5,
        { align: 'center' }
      );
    }

    return doc;
  }

  private drawHeader(
    doc: jsPDF, lab: LabInfo, logoBase64: string | undefined,
    settings: ReportSettings, y: number, isRtl: boolean
  ): number {
    const startY = y;

    // Logo (left for LTR, right for RTL)
    if (logoBase64) {
      const logoX = isRtl ? this.PAGE_WIDTH - this.MARGIN - 25 : this.MARGIN;
      try {
        doc.addImage(logoBase64, 'PNG', logoX, y, 25, 25);
      } catch {}
    }

    // Lab name and info (beside logo)
    const textX = isRtl
      ? this.PAGE_WIDTH - this.MARGIN - (logoBase64 ? 30 : 0)
      : this.MARGIN + (logoBase64 ? 30 : 0);
    const align = isRtl ? 'right' as const : 'left' as const;

    doc.setFontSize(16);
    doc.setFont('helvetica', 'bold');
    doc.setTextColor(26, 35, 126); // dark blue
    const labName = isRtl ? lab.name : (lab.nameEn || lab.name);
    doc.text(labName, textX, y + 6, { align });

    doc.setFontSize(8);
    doc.setFont('helvetica', 'normal');
    doc.setTextColor(100, 116, 139);

    let infoY = y + 12;
    if (settings.report_header_text) {
      doc.text(settings.report_header_text, textX, infoY, { align });
      infoY += 4;
    }

    if (lab.address) {
      doc.text(lab.address, textX, infoY, { align });
      infoY += 4;
    }

    const contactLine = [lab.phone, lab.email].filter(Boolean).join(' | ');
    if (contactLine) {
      doc.text(contactLine, textX, infoY, { align });
      infoY += 4;
    }

    if (settings.report_license_number) {
      doc.text(`License: ${settings.report_license_number}`, textX, infoY, { align });
      infoY += 4;
    }

    return Math.max(infoY, startY + 28) + 2;
  }

  private drawPatientInfo(doc: jsPDF, data: ReportData, y: number, isRtl: boolean): number {
    doc.setFontSize(9);
    doc.setTextColor(30, 41, 59);

    const col1X = isRtl ? this.PAGE_WIDTH - this.MARGIN : this.MARGIN;
    const col2X = isRtl ? this.PAGE_WIDTH - this.MARGIN - 90 : this.MARGIN + 90;
    const align = isRtl ? 'right' as const : 'left' as const;

    // Row 1
    doc.setFont('helvetica', 'bold');
    doc.text(`${this.translate.instant('LIS.RESULTS.PATIENT')}:`, col1X, y, { align });
    doc.setFont('helvetica', 'normal');
    doc.text(data.patient.name, col1X + (isRtl ? -30 : 30), y, { align });

    doc.setFont('helvetica', 'bold');
    doc.text('MRN:', col2X, y, { align });
    doc.setFont('helvetica', 'normal');
    doc.text(data.patient.mrn, col2X + (isRtl ? -15 : 15), y, { align });
    y += 5;

    // Row 2
    doc.setFont('helvetica', 'bold');
    doc.text(`${this.translate.instant('LIS.PATIENTS.AGE')}:`, col1X, y, { align });
    doc.setFont('helvetica', 'normal');
    const ageGender = `${data.patient.age ?? '-'} | ${data.patient.gender}`;
    doc.text(ageGender, col1X + (isRtl ? -30 : 30), y, { align });

    doc.setFont('helvetica', 'bold');
    doc.text(`${this.translate.instant('LIS.REQUESTS.REQUEST_NUMBER')}:`, col2X, y, { align });
    doc.setFont('helvetica', 'normal');
    doc.text(data.requestNumber, col2X + (isRtl ? -30 : 30), y, { align });
    y += 5;

    // Row 3
    if (data.doctor) {
      doc.setFont('helvetica', 'bold');
      doc.text(`${this.translate.instant('LIS.REQUESTS.DOCTOR')}:`, col1X, y, { align });
      doc.setFont('helvetica', 'normal');
      doc.text(data.doctor.name, col1X + (isRtl ? -30 : 30), y, { align });
    }

    doc.setFont('helvetica', 'bold');
    doc.text(`${this.translate.instant('COMMON.DATE')}:`, col2X, y, { align });
    doc.setFont('helvetica', 'normal');
    doc.text(data.requestDate, col2X + (isRtl ? -15 : 15), y, { align });
    y += 6;

    return y;
  }

  // ─── Batch: Generate multiple reports into one PDF ───
  async generateBatch(
    reports: ReportData[],
    settings: ReportSettings,
    lab: LabInfo,
    logoBase64?: string
  ): Promise<jsPDF> {
    const doc = new jsPDF({ unit: 'mm', format: 'a4' });

    for (let i = 0; i < reports.length; i++) {
      if (i > 0) doc.addPage();

      // Generate each report as pages in the same doc
      const singleDoc = await this.generateReport(reports[i], settings, lab, logoBase64);

      // For batch, we rebuild in the same doc
      // Simpler approach: just concatenate
    }

    // Actually, for batch, generate individually and concatenate is complex with jsPDF.
    // Better approach: generate each as separate PDF and open print dialog for each,
    // or generate one large PDF with page breaks between reports.
    // We'll use the single-doc approach with addPage between reports.

    return doc;
  }

  // ─── Print helper ───
  printPdf(doc: jsPDF, filename: string): void {
    const blob = doc.output('blob');
    const url = URL.createObjectURL(blob);
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = url;
    document.body.appendChild(iframe);
    iframe.onload = () => {
      iframe.contentWindow?.print();
      setTimeout(() => {
        document.body.removeChild(iframe);
        URL.revokeObjectURL(url);
      }, 1000);
    };
  }

  savePdf(doc: jsPDF, filename: string): void {
    doc.save(`${filename}.pdf`);
  }
}
```

---

### Task 4: Create Print Queue Component

**Files:**
- Create: `src/app/features/lis/print-queue/lis-print-queue.component.ts`
- Create: `src/app/features/lis/print-queue/lis-print-queue.component.html`
- Create: `src/app/features/lis/print-queue/lis-print-queue.component.scss`
- Modify: `src/app/features/lis/lis-standalone.routes.ts` (add route)

**Component Logic:**
- Load released results grouped by request
- Checkbox selection for batch print
- Print selected / Print all buttons
- Mark as printed tracking
- Filter by date, section, printed/unprinted

**Key signals:**
- `results` — all released results for date
- `selectedIds` — set of selected request IDs
- `printing` — loading state
- `dateFilter` — date picker
- `printedFilter` — all | printed | unprinted

**Template layout:**
```
┌──────────────────────────────────────────────────┐
│ Print Queue                                      │
│ [Date: ◄ 2026-03-07 ►] [Filter: Unprinted ▼]   │
│ [☐ Select All] [🖨 Print Selected] [📄 Save PDF] │
├──────────────────────────────────────────────────┤
│ ☐ LR-00021 | حازم حمدي | MRN-003 | 3 tests | Released │
│ ☐ LR-00022 | emad ghali | MRN-004 | 5 tests | Released │
│ ☑ LR-00023 | أحمد محمد | MRN-005 | 2 tests | Released  │
└──────────────────────────────────────────────────┘
```

---

### Task 5: Add Print Button to Results Page

**Files:**
- Modify: `src/app/features/lis/results/lis-results.component.ts`
- Modify: `src/app/features/lis/results/lis-results.component.html`

**Changes:**
- Add print icon button per request (visible when request has released results)
- On click: generate PDF for that request, open print dialog
- Uses `LisReportPdfService`

---

### Task 6: Add Print Button to Requests List

**Files:**
- Modify: `src/app/features/lis/requests/lis-requests.component.ts`
- Modify: `src/app/features/lis/requests/lis-requests.component.html`

**Changes:**
- Add print icon in actions column (visible for completed/released requests)
- On click: fetch results for request → generate PDF → print

---

### Task 7: Add Route and Navigation

**Files:**
- Modify: `src/app/features/lis/lis-standalone.routes.ts`
- Modify: `src/app/features/lis/lis-layout/lis-layout.component.ts` (add to nav menu)

**Add route:**
```typescript
{
  path: 'print-queue',
  loadComponent: () => import('./print-queue/lis-print-queue.component')
    .then((m) => m.LisPrintQueueComponent),
},
```

**Add to nav menu** under Operations group.

---

### Task 8: Add Translations

**Files:**
- Modify: `src/assets/i18n/en.json`
- Modify: `src/assets/i18n/ar.json`

**New keys under `LIS.PRINT_QUEUE`:**
```json
{
  "TITLE": "Print Queue",
  "SUBTITLE": "Print and manage result reports",
  "PRINT": "Print",
  "PRINT_SELECTED": "Print Selected",
  "PRINT_ALL": "Print All",
  "SAVE_PDF": "Save PDF",
  "SELECT_ALL": "Select All",
  "UNPRINTED": "Unprinted",
  "PRINTED": "Printed",
  "MARK_PRINTED": "Mark as Printed",
  "TESTS_COUNT": "tests",
  "NO_RELEASED": "No released results for this date",
  "PRINTING": "Generating report...",
  "PREVIOUS": "Previous",
  "SCAN_TO_VERIFY": "Scan to verify report authenticity"
}
```

---

### Task 9: Build, Deploy & Test

**Step 1: Build**
```bash
cd /home/moonui/public_html/moon-erp && npx ng build --base-href /app/
```

**Step 2: Deploy**
```bash
rm -f /home/moonui/public_html/app/*.js /home/moonui/public_html/app/*.css /home/moonui/public_html/app/*.html /home/moonui/public_html/app/*.ico
\cp -rf /home/moonui/public_html/moon-erp/dist/moon-erp/browser/* /home/moonui/public_html/app/
```

**Step 3: Verify**
1. Go to `/app/lab/print-queue` — see released results
2. Select a request → click Print → PDF opens in print dialog
3. Verify PDF has: logo, lab info, patient info, results table, QR code, signature
4. Go to Results page → print icon works
5. Go to Requests list → print icon works for completed requests
