Merge pull request #5 from Kenearos/claude/fix-latex-form-filling-GbQtu
Add LaTeX template support for form generation
This commit is contained in:
commit
e84752fff5
10 changed files with 1557 additions and 32 deletions
67
Dockerfile
Normal file
67
Dockerfile
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# Multi-stage Dockerfile for AutoForm AI with LaTeX support
|
||||||
|
|
||||||
|
# Stage 1: Build Frontend
|
||||||
|
FROM node:20-alpine AS frontend-builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the frontend
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: Production image with Python and LaTeX
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Install TeX Live (minimal installation for form generation)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
texlive-latex-base \
|
||||||
|
texlive-latex-recommended \
|
||||||
|
texlive-latex-extra \
|
||||||
|
texlive-fonts-recommended \
|
||||||
|
texlive-lang-german \
|
||||||
|
texlive-plain-generic \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy Python requirements and install
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Install serve for static file serving
|
||||||
|
RUN npm install -g serve
|
||||||
|
|
||||||
|
# Copy the built frontend
|
||||||
|
COPY --from=frontend-builder /app/dist ./dist
|
||||||
|
|
||||||
|
# Copy Python backend and LaTeX templates
|
||||||
|
COPY latex_service.py ./
|
||||||
|
COPY server.py ./
|
||||||
|
COPY templates ./templates
|
||||||
|
|
||||||
|
# Create startup script
|
||||||
|
RUN echo '#!/bin/bash\n\
|
||||||
|
python server.py &\n\
|
||||||
|
serve dist -l ${PORT:-3000}\n\
|
||||||
|
' > /app/start.sh && chmod +x /app/start.sh
|
||||||
|
|
||||||
|
# Expose ports
|
||||||
|
EXPOSE 3000 5000
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV FLASK_DEBUG=false
|
||||||
|
ENV VITE_LATEX_API_URL=http://localhost:5000
|
||||||
|
|
||||||
|
# Start both services
|
||||||
|
CMD ["/app/start.sh"]
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
import { ExtractedField, FileData } from '../types';
|
import { ExtractedField, FileData } from '../types';
|
||||||
import { Check, Edit2, Download, RefreshCw, FileText, AlertTriangle, XCircle, ArrowRight, PenTool, CheckCircle2, Circle } from 'lucide-react';
|
import { Check, Edit2, Download, RefreshCw, FileText, AlertTriangle, XCircle, ArrowRight, PenTool, CheckCircle2, Circle, FileCode, Loader2 } from 'lucide-react';
|
||||||
import { createFilledPdf } from '../services/pdfService';
|
import { createFilledPdf } from '../services/pdfService';
|
||||||
|
import { generateLatexPdf, isLatexServiceAvailable, detectTemplate, base64ToBlob } from '../services/latexService';
|
||||||
import { jsPDF } from "jspdf";
|
import { jsPDF } from "jspdf";
|
||||||
|
|
||||||
interface ReviewPanelProps {
|
interface ReviewPanelProps {
|
||||||
|
|
@ -13,6 +14,8 @@ interface ReviewPanelProps {
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PdfMode = 'overlay' | 'fillable' | 'latex';
|
||||||
|
|
||||||
export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
fields: initialFields,
|
fields: initialFields,
|
||||||
formFile,
|
formFile,
|
||||||
|
|
@ -25,6 +28,26 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
const [activeField, setActiveField] = useState<number | null>(null);
|
const [activeField, setActiveField] = useState<number | null>(null);
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
const [filterMode, setFilterMode] = useState<'ALL' | 'ATTENTION'>('ALL');
|
const [filterMode, setFilterMode] = useState<'ALL' | 'ATTENTION'>('ALL');
|
||||||
|
const [latexAvailable, setLatexAvailable] = useState<boolean | null>(null);
|
||||||
|
const [pdfMode, setPdfMode] = useState<PdfMode>(isFillablePdf ? 'fillable' : 'overlay');
|
||||||
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
|
const [latexPdfBase64, setLatexPdfBase64] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Detect template from form file name
|
||||||
|
const detectedTemplate = useMemo(() => detectTemplate(formFile.file.name), [formFile.file.name]);
|
||||||
|
|
||||||
|
// Check LaTeX service availability on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const checkLatex = async () => {
|
||||||
|
const available = await isLatexServiceAvailable();
|
||||||
|
setLatexAvailable(available);
|
||||||
|
// Auto-switch to LaTeX mode if available and template detected
|
||||||
|
if (available && detectedTemplate && !isFillablePdf) {
|
||||||
|
setPdfMode('latex');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkLatex();
|
||||||
|
}, [detectedTemplate, isFillablePdf]);
|
||||||
|
|
||||||
// Derived state for progress
|
// Derived state for progress
|
||||||
const verifiedCount = fields.filter(f => f.isVerified).length;
|
const verifiedCount = fields.filter(f => f.isVerified).length;
|
||||||
|
|
@ -35,12 +58,46 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
fields.filter(f => f.validation?.status !== 'VALID'),
|
fields.filter(f => f.validation?.status !== 'VALID'),
|
||||||
[fields]);
|
[fields]);
|
||||||
|
|
||||||
|
// Generate LaTeX PDF
|
||||||
|
const generateLatexPreview = useCallback(async () => {
|
||||||
|
if (!detectedTemplate || pdfMode !== 'latex') return;
|
||||||
|
|
||||||
|
setIsGenerating(true);
|
||||||
|
try {
|
||||||
|
const result = await generateLatexPdf(detectedTemplate, fields);
|
||||||
|
if (result.success && result.pdf) {
|
||||||
|
setLatexPdfBase64(result.pdf);
|
||||||
|
const blob = base64ToBlob(result.pdf);
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
setPreviewUrl(url);
|
||||||
|
} else {
|
||||||
|
console.error('LaTeX generation failed:', result.error);
|
||||||
|
// Fallback to overlay mode
|
||||||
|
setPdfMode('overlay');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('LaTeX generation error:', e);
|
||||||
|
setPdfMode('overlay');
|
||||||
|
} finally {
|
||||||
|
setIsGenerating(false);
|
||||||
|
}
|
||||||
|
}, [detectedTemplate, fields, pdfMode]);
|
||||||
|
|
||||||
// Generate preview
|
// Generate preview
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updatePreview = async () => {
|
const updatePreview = async () => {
|
||||||
|
// LaTeX mode - handled separately with button click
|
||||||
|
if (pdfMode === 'latex') {
|
||||||
|
// Only auto-generate if we don't have a preview yet
|
||||||
|
if (!latexPdfBase64) {
|
||||||
|
generateLatexPreview();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (formFile.type === 'application/pdf') {
|
if (formFile.type === 'application/pdf') {
|
||||||
try {
|
try {
|
||||||
const filledPdfBytes = await createFilledPdf(formFile.base64, fields, isFillablePdf);
|
const filledPdfBytes = await createFilledPdf(formFile.base64, fields, pdfMode === 'fillable');
|
||||||
const blob = new Blob([filledPdfBytes], { type: 'application/pdf' });
|
const blob = new Blob([filledPdfBytes], { type: 'application/pdf' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
setPreviewUrl(url);
|
setPreviewUrl(url);
|
||||||
|
|
@ -56,7 +113,7 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
// Debounce slightly to avoid rapid updates on typing
|
// Debounce slightly to avoid rapid updates on typing
|
||||||
const timer = setTimeout(updatePreview, 600);
|
const timer = setTimeout(updatePreview, 600);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [fields, isFillablePdf, formFile.base64, formFile.type]);
|
}, [fields, pdfMode, formFile.base64, formFile.type, latexPdfBase64, generateLatexPreview]);
|
||||||
|
|
||||||
const handleUpdate = (index: number, newValue: string) => {
|
const handleUpdate = (index: number, newValue: string) => {
|
||||||
const newFields = [...fields];
|
const newFields = [...fields];
|
||||||
|
|
@ -87,6 +144,30 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
|
// LaTeX mode - regenerate fresh PDF for download
|
||||||
|
if (pdfMode === 'latex' && detectedTemplate) {
|
||||||
|
setIsGenerating(true);
|
||||||
|
try {
|
||||||
|
const result = await generateLatexPdf(detectedTemplate, fields);
|
||||||
|
if (result.success && result.pdf) {
|
||||||
|
const blob = base64ToBlob(result.pdf);
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${detectedTemplate}_filled.pdf`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} else {
|
||||||
|
alert(`PDF generation failed: ${result.error}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsGenerating(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (formFile.type === 'application/pdf' && previewUrl) {
|
if (formFile.type === 'application/pdf' && previewUrl) {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = previewUrl;
|
a.href = previewUrl;
|
||||||
|
|
@ -106,6 +187,12 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Regenerate LaTeX preview
|
||||||
|
const handleRegenerateLatex = () => {
|
||||||
|
setLatexPdfBase64(null);
|
||||||
|
generateLatexPreview();
|
||||||
|
};
|
||||||
|
|
||||||
// Sort: Unverified/Issues first, then verified
|
// Sort: Unverified/Issues first, then verified
|
||||||
const displayedFields = fields.map((f, i) => ({ ...f, originalIndex: i }))
|
const displayedFields = fields.map((f, i) => ({ ...f, originalIndex: i }))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
|
@ -156,9 +243,14 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
className={`flex items-center px-4 py-2 text-sm font-medium text-white rounded-lg transition-colors shadow-sm bg-indigo-600 hover:bg-indigo-700`}
|
disabled={isGenerating}
|
||||||
|
className={`flex items-center px-4 py-2 text-sm font-medium text-white rounded-lg transition-colors shadow-sm ${isGenerating ? 'bg-indigo-400 cursor-wait' : 'bg-indigo-600 hover:bg-indigo-700'}`}
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4 mr-2" />
|
{isGenerating ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
Download PDF
|
Download PDF
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -170,18 +262,62 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
<div className="p-3 bg-slate-800 border-b border-slate-700 flex justify-between items-center">
|
<div className="p-3 bg-slate-800 border-b border-slate-700 flex justify-between items-center">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-xs font-medium text-slate-300 uppercase tracking-wider">Preview</span>
|
<span className="text-xs font-medium text-slate-300 uppercase tracking-wider">Preview</span>
|
||||||
{!isFillablePdf && (
|
{pdfMode === 'latex' && (
|
||||||
|
<span className="flex items-center text-[10px] bg-emerald-500/20 text-emerald-300 px-1.5 py-0.5 rounded border border-emerald-500/30">
|
||||||
|
<FileCode className="w-3 h-3 mr-1" />
|
||||||
|
LaTeX Template Mode
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{pdfMode === 'overlay' && (
|
||||||
<span className="flex items-center text-[10px] bg-indigo-500/20 text-indigo-300 px-1.5 py-0.5 rounded border border-indigo-500/30">
|
<span className="flex items-center text-[10px] bg-indigo-500/20 text-indigo-300 px-1.5 py-0.5 rounded border border-indigo-500/30">
|
||||||
<PenTool className="w-3 h-3 mr-1" />
|
<PenTool className="w-3 h-3 mr-1" />
|
||||||
Visual Overlay Mode
|
Visual Overlay Mode
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{pdfMode === 'fillable' && (
|
||||||
|
<span className="flex items-center text-[10px] bg-blue-500/20 text-blue-300 px-1.5 py-0.5 rounded border border-blue-500/30">
|
||||||
|
<FileText className="w-3 h-3 mr-1" />
|
||||||
|
Fillable PDF Mode
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{/* Mode Switcher */}
|
||||||
|
{latexAvailable && detectedTemplate && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const newMode = pdfMode === 'latex' ? 'overlay' : 'latex';
|
||||||
|
setPdfMode(newMode);
|
||||||
|
if (newMode === 'latex') {
|
||||||
|
setLatexPdfBase64(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-[10px] bg-slate-700 hover:bg-slate-600 text-slate-300 px-2 py-1 rounded transition-colors"
|
||||||
|
>
|
||||||
|
{pdfMode === 'latex' ? 'Use Overlay' : 'Use LaTeX'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{pdfMode === 'latex' && (
|
||||||
|
<button
|
||||||
|
onClick={handleRegenerateLatex}
|
||||||
|
disabled={isGenerating}
|
||||||
|
className="text-[10px] bg-emerald-700 hover:bg-emerald-600 text-emerald-100 px-2 py-1 rounded transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isGenerating ? 'Generating...' : 'Regenerate'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<span className="text-xs text-slate-400">{formFile.file.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-slate-400">{formFile.file.name}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 bg-slate-900 relative">
|
<div className="flex-1 bg-slate-900 relative">
|
||||||
{previewUrl ? (
|
{isGenerating && pdfMode === 'latex' ? (
|
||||||
formFile.type === 'application/pdf' ? (
|
<div className="flex flex-col items-center justify-center h-full text-emerald-400">
|
||||||
|
<Loader2 className="w-16 h-16 mb-4 animate-spin" />
|
||||||
|
<p className="font-medium">Generating LaTeX PDF...</p>
|
||||||
|
<p className="text-sm text-slate-500 mt-1">Compiling template with your data</p>
|
||||||
|
</div>
|
||||||
|
) : previewUrl ? (
|
||||||
|
formFile.type === 'application/pdf' || pdfMode === 'latex' ? (
|
||||||
<iframe
|
<iframe
|
||||||
src={previewUrl}
|
src={previewUrl}
|
||||||
title="Form PDF Preview"
|
title="Form PDF Preview"
|
||||||
|
|
@ -230,12 +366,24 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isFillablePdf && (
|
{pdfMode === 'latex' && detectedTemplate && (
|
||||||
|
<div className="flex items-start space-x-2 bg-emerald-50 text-emerald-800 p-3 rounded-md border border-emerald-100 text-xs">
|
||||||
|
<FileCode className="w-4 h-4 flex-shrink-0 text-emerald-600 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">LaTeX Template Mode: {detectedTemplate}</p>
|
||||||
|
<p>Using precise LaTeX template. All fields will be positioned correctly in the generated PDF.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{pdfMode === 'overlay' && (
|
||||||
<div className="flex items-start space-x-2 bg-indigo-50 text-indigo-800 p-3 rounded-md border border-indigo-100 text-xs">
|
<div className="flex items-start space-x-2 bg-indigo-50 text-indigo-800 p-3 rounded-md border border-indigo-100 text-xs">
|
||||||
<PenTool className="w-4 h-4 flex-shrink-0 text-indigo-600 mt-0.5" />
|
<PenTool className="w-4 h-4 flex-shrink-0 text-indigo-600 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold">Visual Overlay Mode</p>
|
<p className="font-semibold">Visual Overlay Mode</p>
|
||||||
<p>AI is visually estimating field positions. Please verify text alignment in the preview.</p>
|
<p>AI is visually estimating field positions. Please verify text alignment in the preview.</p>
|
||||||
|
{latexAvailable && detectedTemplate && (
|
||||||
|
<p className="mt-1 text-emerald-700 font-medium">Tip: LaTeX template available! Click "Use LaTeX" for better results.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
249
latex_service.py
Normal file
249
latex_service.py
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
LaTeX Form Generation Service
|
||||||
|
|
||||||
|
This service generates filled PDF forms using LaTeX templates.
|
||||||
|
It takes extracted field data and compiles a LaTeX template into a PDF.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import base64
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
# Template directory
|
||||||
|
TEMPLATE_DIR = Path(__file__).parent / "templates"
|
||||||
|
|
||||||
|
|
||||||
|
def escape_latex(text: str) -> str:
|
||||||
|
"""Escape special LaTeX characters in text."""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# LaTeX special characters that need escaping
|
||||||
|
replacements = [
|
||||||
|
('\\', r'\textbackslash{}'),
|
||||||
|
('&', r'\&'),
|
||||||
|
('%', r'\%'),
|
||||||
|
('$', r'\$'),
|
||||||
|
('#', r'\#'),
|
||||||
|
('_', r'\_'),
|
||||||
|
('{', r'\{'),
|
||||||
|
('}', r'\}'),
|
||||||
|
('~', r'\textasciitilde{}'),
|
||||||
|
('^', r'\textasciicircum{}'),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = text
|
||||||
|
for old, new in replacements:
|
||||||
|
result = result.replace(old, new)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def checkbox(value: str) -> str:
|
||||||
|
"""Return LaTeX checkbox symbol based on value."""
|
||||||
|
if not value:
|
||||||
|
return r'$\square$'
|
||||||
|
|
||||||
|
val_lower = value.lower().strip()
|
||||||
|
if val_lower in ('true', 'yes', 'ja', 'x', '1', 'checked'):
|
||||||
|
return r'$\boxtimes$'
|
||||||
|
return r'$\square$'
|
||||||
|
|
||||||
|
|
||||||
|
def format_date(date_str: str) -> str:
|
||||||
|
"""Ensure date is in DD.MM.YYYY format."""
|
||||||
|
if not date_str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Already in correct format
|
||||||
|
if len(date_str) == 10 and date_str[2] == '.' and date_str[5] == '.':
|
||||||
|
return escape_latex(date_str)
|
||||||
|
|
||||||
|
# Try to parse ISO format
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
for fmt in ['%Y-%m-%d', '%d/%m/%Y', '%m/%d/%Y']:
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(date_str, fmt)
|
||||||
|
return dt.strftime('%d.%m.%Y')
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return escape_latex(date_str)
|
||||||
|
|
||||||
|
|
||||||
|
def load_template(template_name: str) -> str:
|
||||||
|
"""Load a LaTeX template file."""
|
||||||
|
template_path = TEMPLATE_DIR / f"{template_name}.tex"
|
||||||
|
|
||||||
|
if not template_path.exists():
|
||||||
|
raise FileNotFoundError(f"Template not found: {template_path}")
|
||||||
|
|
||||||
|
return template_path.read_text(encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def fill_template(template: str, fields: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Fill a LaTeX template with field values.
|
||||||
|
|
||||||
|
Fields can be accessed in template as:
|
||||||
|
- {{field_name}} for escaped text values
|
||||||
|
- {{field_name|raw}} for raw values (no escaping)
|
||||||
|
- {{field_name|checkbox}} for checkbox symbols
|
||||||
|
- {{field_name|date}} for date formatting
|
||||||
|
"""
|
||||||
|
result = template
|
||||||
|
|
||||||
|
# Process each field
|
||||||
|
for key, value in fields.items():
|
||||||
|
value_str = str(value) if value is not None else ""
|
||||||
|
|
||||||
|
# Replace with different formatters
|
||||||
|
# Raw (no escaping)
|
||||||
|
result = result.replace(f'{{{{{key}|raw}}}}', value_str)
|
||||||
|
# Checkbox
|
||||||
|
result = result.replace(f'{{{{{key}|checkbox}}}}', checkbox(value_str))
|
||||||
|
# Date
|
||||||
|
result = result.replace(f'{{{{{key}|date}}}}', format_date(value_str))
|
||||||
|
# Default (escaped)
|
||||||
|
result = result.replace(f'{{{{{key}}}}}', escape_latex(value_str))
|
||||||
|
|
||||||
|
# Clean up any remaining placeholders (unfilled fields)
|
||||||
|
import re
|
||||||
|
result = re.sub(r'\{\{[^}]+\}\}', '', result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def compile_latex(latex_content: str, output_format: str = 'pdf') -> bytes:
|
||||||
|
"""
|
||||||
|
Compile LaTeX content to PDF.
|
||||||
|
|
||||||
|
Returns the PDF as bytes.
|
||||||
|
"""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
tex_file = Path(tmpdir) / "document.tex"
|
||||||
|
tex_file.write_text(latex_content, encoding='utf-8')
|
||||||
|
|
||||||
|
# Copy any additional files (images, etc.) if needed
|
||||||
|
# For now, we just compile the main document
|
||||||
|
|
||||||
|
# Run pdflatex twice (for references)
|
||||||
|
for _ in range(2):
|
||||||
|
result = subprocess.run(
|
||||||
|
['pdflatex', '-interaction=nonstopmode', '-output-directory', tmpdir, str(tex_file)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
# Check for common errors
|
||||||
|
error_log = Path(tmpdir) / "document.log"
|
||||||
|
if error_log.exists():
|
||||||
|
log_content = error_log.read_text(encoding='utf-8', errors='ignore')
|
||||||
|
# Extract error lines
|
||||||
|
errors = [line for line in log_content.split('\n') if line.startswith('!')]
|
||||||
|
if errors:
|
||||||
|
raise RuntimeError(f"LaTeX compilation failed: {'; '.join(errors[:3])}")
|
||||||
|
raise RuntimeError(f"LaTeX compilation failed: {result.stderr[:500]}")
|
||||||
|
|
||||||
|
pdf_file = Path(tmpdir) / "document.pdf"
|
||||||
|
if not pdf_file.exists():
|
||||||
|
raise RuntimeError("PDF file was not created")
|
||||||
|
|
||||||
|
return pdf_file.read_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_form(template_name: str, fields: Dict[str, Any]) -> bytes:
|
||||||
|
"""
|
||||||
|
Generate a filled PDF form from a template and field data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template_name: Name of the template (without .tex extension)
|
||||||
|
fields: Dictionary of field names to values
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PDF content as bytes
|
||||||
|
"""
|
||||||
|
template = load_template(template_name)
|
||||||
|
filled = fill_template(template, fields)
|
||||||
|
return compile_latex(filled)
|
||||||
|
|
||||||
|
|
||||||
|
def list_templates() -> list:
|
||||||
|
"""List available templates."""
|
||||||
|
if not TEMPLATE_DIR.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [f.stem for f in TEMPLATE_DIR.glob("*.tex")]
|
||||||
|
|
||||||
|
|
||||||
|
# CLI Interface
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='LaTeX Form Generation Service')
|
||||||
|
parser.add_argument('command', choices=['generate', 'list', 'preview'],
|
||||||
|
help='Command to execute')
|
||||||
|
parser.add_argument('--template', '-t', help='Template name')
|
||||||
|
parser.add_argument('--fields', '-f', help='JSON string or file path with field data')
|
||||||
|
parser.add_argument('--output', '-o', help='Output file path')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == 'list':
|
||||||
|
templates = list_templates()
|
||||||
|
print(json.dumps(templates))
|
||||||
|
|
||||||
|
elif args.command == 'preview':
|
||||||
|
# Output the filled LaTeX source (for debugging)
|
||||||
|
if not args.template or not args.fields:
|
||||||
|
print("Error: --template and --fields required", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if args.fields.startswith('{'):
|
||||||
|
fields = json.loads(args.fields)
|
||||||
|
else:
|
||||||
|
with open(args.fields, 'r') as f:
|
||||||
|
fields = json.load(f)
|
||||||
|
|
||||||
|
template = load_template(args.template)
|
||||||
|
filled = fill_template(template, fields)
|
||||||
|
print(filled)
|
||||||
|
|
||||||
|
elif args.command == 'generate':
|
||||||
|
if not args.template or not args.fields:
|
||||||
|
print("Error: --template and --fields required", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Parse fields
|
||||||
|
if args.fields.startswith('{'):
|
||||||
|
fields = json.loads(args.fields)
|
||||||
|
else:
|
||||||
|
with open(args.fields, 'r') as f:
|
||||||
|
fields = json.load(f)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pdf_bytes = generate_form(args.template, fields)
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
with open(args.output, 'wb') as f:
|
||||||
|
f.write(pdf_bytes)
|
||||||
|
print(f"PDF written to {args.output}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
# Output base64 encoded PDF to stdout
|
||||||
|
print(base64.b64encode(pdf_bytes).decode('ascii'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
@ -11,7 +11,9 @@
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
"test:coverage": "vitest --coverage",
|
"test:coverage": "vitest --coverage",
|
||||||
"test:run": "vitest run"
|
"test:run": "vitest run",
|
||||||
|
"latex:server": "python3 server.py",
|
||||||
|
"dev:all": "concurrently \"npm run dev\" \"npm run latex:server\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
[build]
|
[build]
|
||||||
builder = "nixpacks"
|
builder = "dockerfile"
|
||||||
|
dockerfilePath = "Dockerfile"
|
||||||
|
|
||||||
[deploy]
|
[deploy]
|
||||||
startCommand = "npm start"
|
|
||||||
healthcheckPath = "/"
|
healthcheckPath = "/"
|
||||||
healthcheckTimeout = 100
|
healthcheckTimeout = 300
|
||||||
restartPolicyType = "on_failure"
|
restartPolicyType = "on_failure"
|
||||||
restartPolicyMaxRetries = 3
|
restartPolicyMaxRetries = 3
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
pypdf>=4.0.0
|
pypdf>=4.0.0
|
||||||
|
flask>=3.0.0
|
||||||
|
flask-cors>=4.0.0
|
||||||
|
|
|
||||||
337
server.py
Normal file
337
server.py
Normal file
|
|
@ -0,0 +1,337 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Flask server for LaTeX form generation.
|
||||||
|
Provides API endpoints for compiling LaTeX templates with field data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask, request, jsonify, send_file
|
||||||
|
from flask_cors import CORS
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add parent directory to path for imports
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
|
from latex_service import generate_form, list_templates, load_template, fill_template, escape_latex
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app) # Enable CORS for frontend access
|
||||||
|
|
||||||
|
# Field mapping for G2210-11 template
|
||||||
|
# Maps extracted field labels to LaTeX template variables
|
||||||
|
G2210_FIELD_MAPPING = {
|
||||||
|
# Patient data
|
||||||
|
"versicherungsnummer": ["versicherungsnummer", "vers.nr.", "vers-nr", "rentenversicherungsnummer", "rvnr"],
|
||||||
|
"abt_nr": ["abt.-nr.", "abt-nr", "abteilungsnummer", "aktenzeichen"],
|
||||||
|
"name_vorname": ["name, vorname", "name vorname", "patient", "patientenname", "name des versicherten"],
|
||||||
|
"geburtsdatum": ["geburtsdatum", "geb.", "geb.datum", "geboren am", "geburtstag"],
|
||||||
|
"geschlecht": ["geschlecht", "sex", "m/w/d"],
|
||||||
|
"strasse": ["straße", "strasse", "anschrift", "adresse"],
|
||||||
|
"plz": ["plz", "postleitzahl"],
|
||||||
|
"ort": ["ort", "wohnort", "stadt"],
|
||||||
|
"telefon": ["telefon", "tel.", "tel", "telefonnummer", "rufnummer"],
|
||||||
|
"krankenkasse": ["krankenkasse", "krankenversicherung", "kk", "versicherung"],
|
||||||
|
|
||||||
|
# Employment
|
||||||
|
"beruf_taetigkeit": ["beruf", "tätigkeit", "derzeitige tätigkeit", "beschäftigung", "arbeit"],
|
||||||
|
"arbeitgeber": ["arbeitgeber", "firma", "unternehmen"],
|
||||||
|
"au_seit": ["arbeitsunfähig seit", "au seit", "arbeitsunfähigkeit seit", "krankgeschrieben seit"],
|
||||||
|
"letzte_arbeit": ["letzte arbeitsaufnahme", "letzter arbeitstag", "zuletzt gearbeitet"],
|
||||||
|
|
||||||
|
# Diagnoses
|
||||||
|
"diagnose_1": ["diagnose 1", "hauptdiagnose", "1. diagnose"],
|
||||||
|
"diagnose_1_icd": ["icd 1", "icd-10 1", "diagnose 1 icd"],
|
||||||
|
"diagnose_2": ["diagnose 2", "nebendiagnose 1", "2. diagnose"],
|
||||||
|
"diagnose_2_icd": ["icd 2", "icd-10 2", "diagnose 2 icd"],
|
||||||
|
"diagnose_3": ["diagnose 3", "nebendiagnose 2", "3. diagnose"],
|
||||||
|
"diagnose_3_icd": ["icd 3", "icd-10 3", "diagnose 3 icd"],
|
||||||
|
"diagnose_4": ["diagnose 4", "nebendiagnose 3", "4. diagnose"],
|
||||||
|
"diagnose_4_icd": ["icd 4", "icd-10 4", "diagnose 4 icd"],
|
||||||
|
"diagnose_5": ["diagnose 5", "nebendiagnose 4", "5. diagnose"],
|
||||||
|
"diagnose_5_icd": ["icd 5", "icd-10 5", "diagnose 5 icd"],
|
||||||
|
"diagnose_6": ["diagnose 6", "nebendiagnose 5", "6. diagnose"],
|
||||||
|
"diagnose_6_icd": ["icd 6", "icd-10 6", "diagnose 6 icd"],
|
||||||
|
|
||||||
|
# Anamnesis
|
||||||
|
"anamnese_beschwerden": ["anamnese", "beschwerden", "eigenanamnese", "aktuelle beschwerden", "symptome"],
|
||||||
|
"krankheitsverlauf": ["krankheitsverlauf", "verlauf", "bisherige behandlung", "behandlungsverlauf"],
|
||||||
|
"koerperlicher_befund": ["befund", "körperlicher befund", "aktueller befund", "untersuchungsbefund"],
|
||||||
|
|
||||||
|
# Functional limitations (checkboxes)
|
||||||
|
"mobilitaet_keine": ["mobilität keine", "mobilität: keine"],
|
||||||
|
"mobilitaet_gering": ["mobilität gering", "mobilität: gering"],
|
||||||
|
"mobilitaet_erheblich": ["mobilität erheblich", "mobilität: erheblich"],
|
||||||
|
"selbstversorgung_keine": ["selbstversorgung keine"],
|
||||||
|
"selbstversorgung_gering": ["selbstversorgung gering"],
|
||||||
|
"selbstversorgung_erheblich": ["selbstversorgung erheblich"],
|
||||||
|
"haushalt_keine": ["haushaltsführung keine", "haushalt keine"],
|
||||||
|
"haushalt_gering": ["haushaltsführung gering", "haushalt gering"],
|
||||||
|
"haushalt_erheblich": ["haushaltsführung erheblich", "haushalt erheblich"],
|
||||||
|
"erwerb_keine": ["erwerbstätigkeit keine", "erwerb keine"],
|
||||||
|
"erwerb_gering": ["erwerbstätigkeit gering", "erwerb gering"],
|
||||||
|
"erwerb_erheblich": ["erwerbstätigkeit erheblich", "erwerb erheblich"],
|
||||||
|
"kommunikation_keine": ["kommunikation keine"],
|
||||||
|
"kommunikation_gering": ["kommunikation gering"],
|
||||||
|
"kommunikation_erheblich": ["kommunikation erheblich"],
|
||||||
|
"psyche_keine": ["psychische belastbarkeit keine", "psyche keine"],
|
||||||
|
"psyche_gering": ["psychische belastbarkeit gering", "psyche gering"],
|
||||||
|
"psyche_erheblich": ["psychische belastbarkeit erheblich", "psyche erheblich"],
|
||||||
|
"beeintraechtigungen_erlaeuterung": ["beeinträchtigungen erläuterung", "erläuterungen beeinträchtigungen"],
|
||||||
|
|
||||||
|
# Medication
|
||||||
|
"medikament_1": ["medikament 1", "medikation 1"],
|
||||||
|
"medikament_1_dosis": ["dosis 1", "medikament 1 dosis"],
|
||||||
|
"medikament_1_seit": ["seit 1", "medikament 1 seit"],
|
||||||
|
"medikament_2": ["medikament 2", "medikation 2"],
|
||||||
|
"medikament_2_dosis": ["dosis 2", "medikament 2 dosis"],
|
||||||
|
"medikament_2_seit": ["seit 2", "medikament 2 seit"],
|
||||||
|
"medikament_3": ["medikament 3", "medikation 3"],
|
||||||
|
"medikament_3_dosis": ["dosis 3", "medikament 3 dosis"],
|
||||||
|
"medikament_3_seit": ["seit 3", "medikament 3 seit"],
|
||||||
|
"medikament_4": ["medikament 4", "medikation 4"],
|
||||||
|
"medikament_4_dosis": ["dosis 4", "medikament 4 dosis"],
|
||||||
|
"medikament_4_seit": ["seit 4", "medikament 4 seit"],
|
||||||
|
"medikament_5": ["medikament 5", "medikation 5"],
|
||||||
|
"medikament_5_dosis": ["dosis 5", "medikament 5 dosis"],
|
||||||
|
"medikament_5_seit": ["seit 5", "medikament 5 seit"],
|
||||||
|
"physikalische_therapie": ["physikalische therapie", "heilmittel", "physiotherapie", "krankengymnastik"],
|
||||||
|
|
||||||
|
# Previous rehab
|
||||||
|
"reha_1_zeitraum": ["reha 1 zeitraum", "frühere reha 1 zeitraum"],
|
||||||
|
"reha_1_einrichtung": ["reha 1 einrichtung", "frühere reha 1 einrichtung"],
|
||||||
|
"reha_1_erfolg": ["reha 1 erfolg", "frühere reha 1 erfolg"],
|
||||||
|
"reha_2_zeitraum": ["reha 2 zeitraum", "frühere reha 2 zeitraum"],
|
||||||
|
"reha_2_einrichtung": ["reha 2 einrichtung", "frühere reha 2 einrichtung"],
|
||||||
|
"reha_2_erfolg": ["reha 2 erfolg", "frühere reha 2 erfolg"],
|
||||||
|
|
||||||
|
# Assessment
|
||||||
|
"leistungsvermoegen_checkbox_vollschichtig": ["vollschichtig", "leistungsvermögen vollschichtig", "6 stunden und mehr"],
|
||||||
|
"leistungsvermoegen_checkbox_teilschichtig": ["teilschichtig", "leistungsvermögen 3-6", "3-6 stunden"],
|
||||||
|
"leistungsvermoegen_checkbox_unter3": ["unter 3 stunden", "leistungsvermögen unter 3"],
|
||||||
|
"reha_beduerftig_begruendung": ["rehabilitationsbedürftigkeit", "reha begründung", "reha bedürftigkeit"],
|
||||||
|
"reha_ziel": ["rehabilitationsziel", "reha ziel", "therapieziel"],
|
||||||
|
"reha_stationaer": ["stationär", "stationäre reha"],
|
||||||
|
"reha_ambulant": ["ambulant", "ambulante reha"],
|
||||||
|
"reha_ganztaegig": ["ganztägig ambulant", "teilstationär"],
|
||||||
|
"reha_einrichtung_empfehlung": ["empfohlene einrichtung", "reha einrichtung", "klinikempfehlung"],
|
||||||
|
|
||||||
|
# Travel capability
|
||||||
|
"reisefaehig_ja": ["reisefähig ja", "öffentliche verkehrsmittel ja"],
|
||||||
|
"reisefaehig_nein": ["reisefähig nein", "öffentliche verkehrsmittel nein"],
|
||||||
|
"reisefaehig_begruendung": ["reisefähigkeit begründung", "nicht reisefähig weil"],
|
||||||
|
"begleitperson_ja": ["begleitperson ja", "begleitperson erforderlich"],
|
||||||
|
"begleitperson_nein": ["begleitperson nein", "keine begleitperson"],
|
||||||
|
|
||||||
|
# Additional
|
||||||
|
"ergaenzende_angaben": ["ergänzende angaben", "zusätzliche informationen", "bemerkungen", "sonstiges"],
|
||||||
|
|
||||||
|
# Attachments
|
||||||
|
"anlage_laborbefunde": ["anlage laborbefunde", "laborbefunde"],
|
||||||
|
"anlage_roentgen": ["anlage röntgen", "bildgebende befunde", "röntgenbefunde"],
|
||||||
|
"anlage_arztbriefe": ["anlage arztbriefe", "arztbriefe"],
|
||||||
|
"anlage_krankenhausberichte": ["anlage krankenhausberichte", "krankenhausberichte", "entlassungsberichte"],
|
||||||
|
"anlage_sonstige": ["anlage sonstige", "sonstige anlagen"],
|
||||||
|
"anlage_sonstige_text": ["sonstige anlagen text", "anlage sonstige bezeichnung"],
|
||||||
|
|
||||||
|
# Signature
|
||||||
|
"unterschrift_datum": ["unterschrift datum", "datum unterschrift", "ausstellungsdatum"],
|
||||||
|
"arzt_name": ["arzt name", "name des arztes", "behandelnder arzt"],
|
||||||
|
"arzt_fachrichtung": ["facharztbezeichnung", "fachrichtung", "facharzt"],
|
||||||
|
"praxis_anschrift": ["praxis anschrift", "praxisadresse", "arztpraxis"],
|
||||||
|
"praxis_telefon": ["praxis telefon", "praxis tel"],
|
||||||
|
"bsnr": ["bsnr", "betriebsstättennummer"],
|
||||||
|
"lanr": ["lanr", "lebenslange arztnummer"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_label(label: str) -> str:
|
||||||
|
"""Normalize a label for matching."""
|
||||||
|
return label.lower().strip().replace(':', '').replace('_', ' ')
|
||||||
|
|
||||||
|
|
||||||
|
def map_fields_to_template(extracted_fields: list, template_mapping: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Map extracted fields to template variables using fuzzy matching.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
extracted_fields: List of {label, value, ...} dicts from AI extraction
|
||||||
|
template_mapping: Dict mapping template vars to possible label variations
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict of template variables to values
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# Build reverse mapping: normalized label -> template var
|
||||||
|
reverse_map = {}
|
||||||
|
for template_var, possible_labels in template_mapping.items():
|
||||||
|
for label in possible_labels:
|
||||||
|
reverse_map[normalize_label(label)] = template_var
|
||||||
|
|
||||||
|
# Map each extracted field
|
||||||
|
for field in extracted_fields:
|
||||||
|
label = normalize_label(field.get('label', ''))
|
||||||
|
value = field.get('value', '')
|
||||||
|
|
||||||
|
if not label or not value:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Direct match
|
||||||
|
if label in reverse_map:
|
||||||
|
result[reverse_map[label]] = value
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fuzzy match: check if any mapping label is contained in the extracted label
|
||||||
|
for possible_label, template_var in reverse_map.items():
|
||||||
|
if possible_label in label or label in possible_label:
|
||||||
|
result[template_var] = value
|
||||||
|
break
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/health', methods=['GET'])
|
||||||
|
def health_check():
|
||||||
|
"""Health check endpoint."""
|
||||||
|
return jsonify({'status': 'ok', 'service': 'latex-form-generator'})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/templates', methods=['GET'])
|
||||||
|
def get_templates():
|
||||||
|
"""List available LaTeX templates."""
|
||||||
|
templates = list_templates()
|
||||||
|
return jsonify({'templates': templates})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/generate', methods=['POST'])
|
||||||
|
def generate_pdf():
|
||||||
|
"""
|
||||||
|
Generate a filled PDF from a template and field data.
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
{
|
||||||
|
"template": "G2210-11",
|
||||||
|
"fields": [
|
||||||
|
{"label": "Name, Vorname", "value": "Müller, Hans"},
|
||||||
|
{"label": "Geburtsdatum", "value": "01.01.1970"},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Returns: PDF file or base64-encoded PDF
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return jsonify({'error': 'No JSON data provided'}), 400
|
||||||
|
|
||||||
|
template_name = data.get('template', 'G2210-11')
|
||||||
|
extracted_fields = data.get('fields', [])
|
||||||
|
return_format = data.get('format', 'base64') # 'base64' or 'file'
|
||||||
|
|
||||||
|
# Get the appropriate field mapping
|
||||||
|
if template_name == 'G2210-11':
|
||||||
|
field_mapping = G2210_FIELD_MAPPING
|
||||||
|
else:
|
||||||
|
# For other templates, try direct field names
|
||||||
|
field_mapping = {}
|
||||||
|
|
||||||
|
# Map extracted fields to template variables
|
||||||
|
if field_mapping:
|
||||||
|
template_fields = map_fields_to_template(extracted_fields, field_mapping)
|
||||||
|
else:
|
||||||
|
# Direct mapping: use label as key
|
||||||
|
template_fields = {normalize_label(f['label']).replace(' ', '_'): f['value']
|
||||||
|
for f in extracted_fields if f.get('value')}
|
||||||
|
|
||||||
|
# Generate PDF
|
||||||
|
pdf_bytes = generate_form(template_name, template_fields)
|
||||||
|
|
||||||
|
if return_format == 'file':
|
||||||
|
return send_file(
|
||||||
|
io.BytesIO(pdf_bytes),
|
||||||
|
mimetype='application/pdf',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'{template_name}_filled.pdf'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Return base64
|
||||||
|
pdf_base64 = base64.b64encode(pdf_bytes).decode('ascii')
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'pdf': pdf_base64,
|
||||||
|
'mapped_fields': template_fields
|
||||||
|
})
|
||||||
|
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
return jsonify({'error': str(e)}), 404
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': f'Generation failed: {str(e)}'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/preview', methods=['POST'])
|
||||||
|
def preview_latex():
|
||||||
|
"""
|
||||||
|
Preview the filled LaTeX source (for debugging).
|
||||||
|
|
||||||
|
Same request format as /api/generate.
|
||||||
|
Returns the LaTeX source instead of compiled PDF.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return jsonify({'error': 'No JSON data provided'}), 400
|
||||||
|
|
||||||
|
template_name = data.get('template', 'G2210-11')
|
||||||
|
extracted_fields = data.get('fields', [])
|
||||||
|
|
||||||
|
# Get the appropriate field mapping
|
||||||
|
if template_name == 'G2210-11':
|
||||||
|
field_mapping = G2210_FIELD_MAPPING
|
||||||
|
else:
|
||||||
|
field_mapping = {}
|
||||||
|
|
||||||
|
# Map extracted fields to template variables
|
||||||
|
if field_mapping:
|
||||||
|
template_fields = map_fields_to_template(extracted_fields, field_mapping)
|
||||||
|
else:
|
||||||
|
template_fields = {normalize_label(f['label']).replace(' ', '_'): f['value']
|
||||||
|
for f in extracted_fields if f.get('value')}
|
||||||
|
|
||||||
|
# Load and fill template
|
||||||
|
template = load_template(template_name)
|
||||||
|
filled = fill_template(template, template_fields)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'latex': filled,
|
||||||
|
'mapped_fields': template_fields
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/field-mapping/<template_name>', methods=['GET'])
|
||||||
|
def get_field_mapping(template_name):
|
||||||
|
"""Get the field mapping for a specific template."""
|
||||||
|
if template_name == 'G2210-11':
|
||||||
|
return jsonify({
|
||||||
|
'template': template_name,
|
||||||
|
'fields': list(G2210_FIELD_MAPPING.keys()),
|
||||||
|
'mapping': G2210_FIELD_MAPPING
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'Unknown template'}), 404
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
port = int(os.environ.get('PORT', 5000))
|
||||||
|
debug = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
|
||||||
|
app.run(host='0.0.0.0', port=port, debug=debug)
|
||||||
|
|
@ -2,6 +2,7 @@ import { GoogleGenAI, Type, Schema } from "@google/genai";
|
||||||
import { FileData, FormResponse } from "../types";
|
import { FileData, FormResponse } from "../types";
|
||||||
import { PdfFieldInfo } from "./pdfService";
|
import { PdfFieldInfo } from "./pdfService";
|
||||||
import { getApiKey } from "./apiKeyService";
|
import { getApiKey } from "./apiKeyService";
|
||||||
|
import { detectTemplate, getExpectedFields } from "./latexService";
|
||||||
|
|
||||||
const getAI = () => {
|
const getAI = () => {
|
||||||
const apiKey = getApiKey();
|
const apiKey = getApiKey();
|
||||||
|
|
@ -75,12 +76,98 @@ const responseSchema: Schema = {
|
||||||
required: ["fields", "summary"]
|
required: ["fields", "summary"]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// G2210-11 specific field definitions for better extraction
|
||||||
|
const G2210_FIELDS = `
|
||||||
|
REQUIRED FIELDS FOR G2210-11 (Ärztlicher Befundbericht):
|
||||||
|
Extract ALL of the following fields from the source document:
|
||||||
|
|
||||||
|
PATIENT DATA:
|
||||||
|
- Versicherungsnummer (e.g., "12 345678 A 123")
|
||||||
|
- ABT.-Nr. (Aktenzeichen/Abteilungsnummer)
|
||||||
|
- Name, Vorname (Full name: "Nachname, Vorname")
|
||||||
|
- Geburtsdatum (format: DD.MM.YYYY)
|
||||||
|
- Geschlecht (männlich/weiblich/divers)
|
||||||
|
- Straße, Hausnummer
|
||||||
|
- PLZ
|
||||||
|
- Ort
|
||||||
|
- Telefon
|
||||||
|
- Krankenkasse
|
||||||
|
|
||||||
|
EMPLOYMENT:
|
||||||
|
- Derzeitige Tätigkeit (Beruf)
|
||||||
|
- Arbeitgeber
|
||||||
|
- Arbeitsunfähig seit (date: DD.MM.YYYY)
|
||||||
|
- Letzte Arbeitsaufnahme
|
||||||
|
|
||||||
|
DIAGNOSES (up to 6, with ICD-10 codes):
|
||||||
|
- Diagnose 1 + Diagnose 1 ICD
|
||||||
|
- Diagnose 2 + Diagnose 2 ICD
|
||||||
|
- Diagnose 3 + Diagnose 3 ICD
|
||||||
|
- Diagnose 4 + Diagnose 4 ICD
|
||||||
|
- Diagnose 5 + Diagnose 5 ICD
|
||||||
|
- Diagnose 6 + Diagnose 6 ICD
|
||||||
|
|
||||||
|
ANAMNESIS:
|
||||||
|
- Anamnese/Beschwerden (patient symptoms and history)
|
||||||
|
- Krankheitsverlauf (disease progression, previous treatments)
|
||||||
|
- Körperlicher Befund (physical examination findings)
|
||||||
|
|
||||||
|
FUNCTIONAL LIMITATIONS (mark as "keine", "gering", or "erheblich"):
|
||||||
|
- Mobilität keine/gering/erheblich
|
||||||
|
- Selbstversorgung keine/gering/erheblich
|
||||||
|
- Haushaltsführung keine/gering/erheblich
|
||||||
|
- Erwerbstätigkeit keine/gering/erheblich
|
||||||
|
- Kommunikation keine/gering/erheblich
|
||||||
|
- Psychische Belastbarkeit keine/gering/erheblich
|
||||||
|
- Beeinträchtigungen Erläuterung
|
||||||
|
|
||||||
|
MEDICATION (up to 5):
|
||||||
|
- Medikament 1 + Medikament 1 Dosis + Medikament 1 Seit
|
||||||
|
- Medikament 2 + Medikament 2 Dosis + Medikament 2 Seit
|
||||||
|
- Medikament 3 + Medikament 3 Dosis + Medikament 3 Seit
|
||||||
|
- Medikament 4 + Medikament 4 Dosis + Medikament 4 Seit
|
||||||
|
- Medikament 5 + Medikament 5 Dosis + Medikament 5 Seit
|
||||||
|
- Physikalische Therapie
|
||||||
|
|
||||||
|
PREVIOUS REHABILITATION:
|
||||||
|
- Reha 1 Zeitraum + Reha 1 Einrichtung + Reha 1 Erfolg
|
||||||
|
- Reha 2 Zeitraum + Reha 2 Einrichtung + Reha 2 Erfolg
|
||||||
|
|
||||||
|
ASSESSMENT:
|
||||||
|
- Leistungsvermögen (vollschichtig/3-6 Stunden/unter 3 Stunden)
|
||||||
|
- Rehabilitationsbedürftigkeit (reasoning for rehab need)
|
||||||
|
- Rehabilitationsziel
|
||||||
|
- Rehabilitationsform (stationär/ambulant/ganztägig ambulant)
|
||||||
|
- Reha Einrichtung Empfehlung
|
||||||
|
|
||||||
|
TRAVEL:
|
||||||
|
- Reisefähig (ja/nein)
|
||||||
|
- Reisefähig Begründung (if no)
|
||||||
|
- Begleitperson (ja/nein)
|
||||||
|
|
||||||
|
ADDITIONAL:
|
||||||
|
- Ergänzende Angaben
|
||||||
|
|
||||||
|
DOCTOR INFORMATION:
|
||||||
|
- Arzt Name
|
||||||
|
- Facharztbezeichnung
|
||||||
|
- Praxis Anschrift
|
||||||
|
- Praxis Telefon
|
||||||
|
- BSNR
|
||||||
|
- LANR
|
||||||
|
- Unterschrift Datum
|
||||||
|
`;
|
||||||
|
|
||||||
export const processDocuments = async (
|
export const processDocuments = async (
|
||||||
blankForm: FileData,
|
blankForm: FileData,
|
||||||
sourceDocument: FileData,
|
sourceDocument: FileData,
|
||||||
pdfFields: PdfFieldInfo[] = []
|
pdfFields: PdfFieldInfo[] = []
|
||||||
): Promise<FormResponse> => {
|
): Promise<FormResponse> => {
|
||||||
|
|
||||||
|
// Detect if we have a known template
|
||||||
|
const detectedTemplate = detectTemplate(blankForm.file?.name || '');
|
||||||
|
const expectedFields = detectedTemplate ? getExpectedFields(detectedTemplate) : [];
|
||||||
|
|
||||||
const formPart = {
|
const formPart = {
|
||||||
inlineData: {
|
inlineData: {
|
||||||
data: blankForm.base64,
|
data: blankForm.base64,
|
||||||
|
|
@ -102,7 +189,30 @@ export const processDocuments = async (
|
||||||
CRITICAL INSTRUCTION: You must verify every extraction. If a value is ambiguous, plausibility is low, or you are guessing, set validation.status to 'WARNING' and explain why in validation.message.
|
CRITICAL INSTRUCTION: You must verify every extraction. If a value is ambiguous, plausibility is low, or you are guessing, set validation.status to 'WARNING' and explain why in validation.message.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (pdfFields.length > 0) {
|
// Add template-specific instructions
|
||||||
|
if (detectedTemplate === 'G2210-11') {
|
||||||
|
systemPrompt += `
|
||||||
|
DETECTED FORM: G2210-11 (Ärztlicher Befundbericht der DRV Westfalen)
|
||||||
|
|
||||||
|
${G2210_FIELDS}
|
||||||
|
|
||||||
|
IMPORTANT INSTRUCTIONS:
|
||||||
|
1. Extract ALL fields listed above, even if they are empty in the source.
|
||||||
|
2. Use the EXACT label names as listed above for each field.
|
||||||
|
3. For multi-value fields like diagnoses and medications, create separate field entries.
|
||||||
|
4. For checkbox fields (Mobilität, Selbstversorgung, etc.), return separate fields for each option.
|
||||||
|
Example: If mobility is "erheblich", return:
|
||||||
|
- "Mobilität keine" with value ""
|
||||||
|
- "Mobilität gering" with value ""
|
||||||
|
- "Mobilität erheblich" with value "true"
|
||||||
|
5. ICD-10 codes must be in standard format (e.g., "M54.5", "F32.1")
|
||||||
|
6. Dates must be in DD.MM.YYYY format.
|
||||||
|
7. For Leistungsvermögen, return separate checkbox fields:
|
||||||
|
- "Leistungsvermögen vollschichtig" (true/false)
|
||||||
|
- "Leistungsvermögen 3-6 Stunden" (true/false)
|
||||||
|
- "Leistungsvermögen unter 3 Stunden" (true/false)
|
||||||
|
`;
|
||||||
|
} else if (pdfFields.length > 0) {
|
||||||
const fieldList = pdfFields.map(f => `"${f.name}" (${f.type})`).join(", ");
|
const fieldList = pdfFields.map(f => `"${f.name}" (${f.type})`).join(", ");
|
||||||
systemPrompt += `
|
systemPrompt += `
|
||||||
MODE: FILLABLE PDF (AcroForm).
|
MODE: FILLABLE PDF (AcroForm).
|
||||||
|
|
@ -110,6 +220,12 @@ export const processDocuments = async (
|
||||||
Map extracted data to these exact field IDs: [${fieldList}].
|
Map extracted data to these exact field IDs: [${fieldList}].
|
||||||
Return the 'key' property matching the field ID.
|
Return the 'key' property matching the field ID.
|
||||||
`;
|
`;
|
||||||
|
} else if (expectedFields.length > 0) {
|
||||||
|
systemPrompt += `
|
||||||
|
MODE: TEMPLATE-BASED EXTRACTION.
|
||||||
|
Extract the following specific fields: [${expectedFields.join(", ")}].
|
||||||
|
Use these exact label names in your response.
|
||||||
|
`;
|
||||||
} else {
|
} else {
|
||||||
systemPrompt += `
|
systemPrompt += `
|
||||||
MODE: VISUAL FILLING (Flat/XFA/Scan).
|
MODE: VISUAL FILLING (Flat/XFA/Scan).
|
||||||
|
|
|
||||||
251
services/latexService.ts
Normal file
251
services/latexService.ts
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
/**
|
||||||
|
* LaTeX Form Generation Service
|
||||||
|
*
|
||||||
|
* This service communicates with the Python LaTeX backend to generate
|
||||||
|
* filled PDF forms using LaTeX templates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ExtractedField } from '../types';
|
||||||
|
|
||||||
|
// Backend API URL - can be configured via environment variable
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_LATEX_API_URL || 'http://localhost:5000';
|
||||||
|
|
||||||
|
export interface LatexGenerationResult {
|
||||||
|
success: boolean;
|
||||||
|
pdf?: string; // base64 encoded PDF
|
||||||
|
mappedFields?: Record<string, string>;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateInfo {
|
||||||
|
name: string;
|
||||||
|
fields: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the LaTeX backend is available
|
||||||
|
*/
|
||||||
|
export const isLatexServiceAvailable = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/health`, {
|
||||||
|
method: 'GET',
|
||||||
|
signal: AbortSignal.timeout(3000),
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of available LaTeX templates
|
||||||
|
*/
|
||||||
|
export const getAvailableTemplates = async (): Promise<string[]> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/templates`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch templates');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return data.templates || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not fetch templates:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field mapping for a specific template
|
||||||
|
*/
|
||||||
|
export const getTemplateFieldMapping = async (templateName: string): Promise<Record<string, string[]> | null> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/field-mapping/${templateName}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return data.mapping || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not fetch field mapping:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a filled PDF using LaTeX template
|
||||||
|
*/
|
||||||
|
export const generateLatexPdf = async (
|
||||||
|
templateName: string,
|
||||||
|
fields: ExtractedField[]
|
||||||
|
): Promise<LatexGenerationResult> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/generate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
template: templateName,
|
||||||
|
fields: fields.map(f => ({
|
||||||
|
label: f.label,
|
||||||
|
value: f.value,
|
||||||
|
key: f.key,
|
||||||
|
})),
|
||||||
|
format: 'base64',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.error || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
pdf: data.pdf,
|
||||||
|
mappedFields: data.mapped_fields,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('LaTeX PDF generation failed:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview the filled LaTeX source (for debugging)
|
||||||
|
*/
|
||||||
|
export const previewLatexSource = async (
|
||||||
|
templateName: string,
|
||||||
|
fields: ExtractedField[]
|
||||||
|
): Promise<{ latex?: string; error?: string }> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/preview`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
template: templateName,
|
||||||
|
fields: fields.map(f => ({
|
||||||
|
label: f.label,
|
||||||
|
value: f.value,
|
||||||
|
key: f.key,
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.error || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return { latex: data.latex };
|
||||||
|
} catch (error) {
|
||||||
|
return { error: error instanceof Error ? error.message : 'Unknown error' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert base64 PDF to Blob URL for preview/download
|
||||||
|
*/
|
||||||
|
export const base64ToBlob = (base64: string, mimeType: string = 'application/pdf'): Blob => {
|
||||||
|
const byteCharacters = atob(base64);
|
||||||
|
const byteNumbers = new Array(byteCharacters.length);
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
return new Blob([byteArray], { type: mimeType });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect which template to use based on form file name or content
|
||||||
|
*/
|
||||||
|
export const detectTemplate = (fileName: string): string | null => {
|
||||||
|
const lowerName = fileName.toLowerCase();
|
||||||
|
|
||||||
|
// G2210-11 Ärztlicher Befundbericht
|
||||||
|
if (lowerName.includes('g2210') ||
|
||||||
|
lowerName.includes('befundbericht') ||
|
||||||
|
lowerName.includes('aerztlicher') ||
|
||||||
|
lowerName.includes('ärztlicher')) {
|
||||||
|
return 'G2210-11';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add more template detection patterns here
|
||||||
|
// if (lowerName.includes('s0051')) return 'S0051';
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get expected fields for a known form type
|
||||||
|
* This helps the AI extraction know what fields to look for
|
||||||
|
*/
|
||||||
|
export const getExpectedFields = (templateName: string): string[] => {
|
||||||
|
const fieldMappings: Record<string, string[]> = {
|
||||||
|
'G2210-11': [
|
||||||
|
'Versicherungsnummer',
|
||||||
|
'ABT.-Nr.',
|
||||||
|
'Name, Vorname',
|
||||||
|
'Geburtsdatum',
|
||||||
|
'Geschlecht',
|
||||||
|
'Straße, Hausnummer',
|
||||||
|
'PLZ',
|
||||||
|
'Ort',
|
||||||
|
'Telefon',
|
||||||
|
'Krankenkasse',
|
||||||
|
'Derzeitige Tätigkeit',
|
||||||
|
'Arbeitgeber',
|
||||||
|
'Arbeitsunfähig seit',
|
||||||
|
'Diagnose 1',
|
||||||
|
'Diagnose 1 ICD',
|
||||||
|
'Diagnose 2',
|
||||||
|
'Diagnose 2 ICD',
|
||||||
|
'Diagnose 3',
|
||||||
|
'Diagnose 3 ICD',
|
||||||
|
'Diagnose 4',
|
||||||
|
'Diagnose 4 ICD',
|
||||||
|
'Diagnose 5',
|
||||||
|
'Diagnose 5 ICD',
|
||||||
|
'Diagnose 6',
|
||||||
|
'Diagnose 6 ICD',
|
||||||
|
'Anamnese/Beschwerden',
|
||||||
|
'Krankheitsverlauf',
|
||||||
|
'Körperlicher Befund',
|
||||||
|
'Mobilität (keine/gering/erheblich)',
|
||||||
|
'Selbstversorgung (keine/gering/erheblich)',
|
||||||
|
'Haushaltsführung (keine/gering/erheblich)',
|
||||||
|
'Erwerbstätigkeit (keine/gering/erheblich)',
|
||||||
|
'Medikament 1',
|
||||||
|
'Medikament 1 Dosis',
|
||||||
|
'Medikament 2',
|
||||||
|
'Medikament 2 Dosis',
|
||||||
|
'Medikament 3',
|
||||||
|
'Medikament 3 Dosis',
|
||||||
|
'Physikalische Therapie',
|
||||||
|
'Frühere Reha Zeitraum',
|
||||||
|
'Frühere Reha Einrichtung',
|
||||||
|
'Leistungsvermögen',
|
||||||
|
'Rehabilitationsbedürftigkeit',
|
||||||
|
'Rehabilitationsziel',
|
||||||
|
'Rehabilitationsform (stationär/ambulant)',
|
||||||
|
'Reisefähig (ja/nein)',
|
||||||
|
'Begleitperson erforderlich (ja/nein)',
|
||||||
|
'Ergänzende Angaben',
|
||||||
|
'Arzt Name',
|
||||||
|
'Facharztbezeichnung',
|
||||||
|
'Praxis Anschrift',
|
||||||
|
'Praxis Telefon',
|
||||||
|
'BSNR',
|
||||||
|
'LANR',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return fieldMappings[templateName] || [];
|
||||||
|
};
|
||||||
353
templates/G2210-11.tex
Normal file
353
templates/G2210-11.tex
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
\documentclass[a4paper,10pt]{article}
|
||||||
|
\usepackage[utf8]{inputenc}
|
||||||
|
\usepackage[T1]{fontenc}
|
||||||
|
\usepackage[ngerman]{babel}
|
||||||
|
\usepackage[margin=1.5cm,top=1cm,bottom=1.5cm]{geometry}
|
||||||
|
\usepackage{array}
|
||||||
|
\usepackage{tabularx}
|
||||||
|
\usepackage{booktabs}
|
||||||
|
\usepackage{multirow}
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\usepackage{xcolor}
|
||||||
|
\usepackage{helvet}
|
||||||
|
\usepackage{amssymb}
|
||||||
|
\usepackage{tikz}
|
||||||
|
\usepackage{fancyhdr}
|
||||||
|
\usepackage{lastpage}
|
||||||
|
\usepackage{enumitem}
|
||||||
|
|
||||||
|
\renewcommand{\familydefault}{\sfdefault}
|
||||||
|
\setlength{\parindent}{0pt}
|
||||||
|
\setlength{\parskip}{0.3em}
|
||||||
|
|
||||||
|
% Custom colors
|
||||||
|
\definecolor{drvblue}{RGB}{0,51,102}
|
||||||
|
\definecolor{lightgray}{RGB}{240,240,240}
|
||||||
|
\definecolor{bordergray}{RGB}{180,180,180}
|
||||||
|
|
||||||
|
% Header and Footer
|
||||||
|
\pagestyle{fancy}
|
||||||
|
\fancyhf{}
|
||||||
|
\renewcommand{\headrulewidth}{0pt}
|
||||||
|
\fancyfoot[C]{\footnotesize Seite \thepage\ von \pageref{LastPage}}
|
||||||
|
\fancyfoot[R]{\footnotesize G2210-11 (01.2025)}
|
||||||
|
|
||||||
|
% Custom commands
|
||||||
|
\newcommand{\formfield}[2]{\textbf{#1:} #2}
|
||||||
|
\newcommand{\formbox}[1]{\fbox{\parbox{0.95\linewidth}{#1}}}
|
||||||
|
\newcommand{\sectionheader}[1]{\colorbox{lightgray}{\parbox{\dimexpr\linewidth-2\fboxsep}{\textbf{#1}}}}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
|
||||||
|
% === HEADER ===
|
||||||
|
\begin{center}
|
||||||
|
{\Large\bfseries\color{drvblue} Deutsche Rentenversicherung Westfalen}\\[0.3em]
|
||||||
|
{\large Ärztlicher Befundbericht}\\[0.2em]
|
||||||
|
{\small zum Antrag auf Leistungen zur medizinischen Rehabilitation}\\[0.5em]
|
||||||
|
\hrule
|
||||||
|
\end{center}
|
||||||
|
|
||||||
|
\vspace{0.5em}
|
||||||
|
|
||||||
|
% === PATIENT DATA ===
|
||||||
|
\sectionheader{1. Angaben zur versicherten Person}
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\begin{tabularx}{\linewidth}{|p{4cm}|X|p{4cm}|X|}
|
||||||
|
\hline
|
||||||
|
\textbf{Versicherungsnummer:} & {{versicherungsnummer}} & \textbf{ABT.-Nr.:} & {{abt_nr}} \\
|
||||||
|
\hline
|
||||||
|
\textbf{Name, Vorname:} & \multicolumn{3}{l|}{{{name_vorname}}} \\
|
||||||
|
\hline
|
||||||
|
\textbf{Geburtsdatum:} & {{geburtsdatum|date}} & \textbf{Geschlecht:} & {{geschlecht}} \\
|
||||||
|
\hline
|
||||||
|
\textbf{Straße, Hausnummer:} & \multicolumn{3}{l|}{{{strasse}}} \\
|
||||||
|
\hline
|
||||||
|
\textbf{PLZ, Ort:} & \multicolumn{3}{l|}{{{plz}} {{ort}}} \\
|
||||||
|
\hline
|
||||||
|
\textbf{Telefon:} & {{telefon}} & \textbf{Krankenkasse:} & {{krankenkasse}} \\
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.5em}
|
||||||
|
|
||||||
|
% === EMPLOYMENT ===
|
||||||
|
\sectionheader{2. Angaben zur beruflichen Situation}
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\begin{tabularx}{\linewidth}{|p{5cm}|X|}
|
||||||
|
\hline
|
||||||
|
\textbf{Derzeitige Tätigkeit:} & {{beruf_taetigkeit}} \\
|
||||||
|
\hline
|
||||||
|
\textbf{Arbeitgeber:} & {{arbeitgeber}} \\
|
||||||
|
\hline
|
||||||
|
\textbf{Arbeitsunfähig seit:} & {{au_seit|date}} \\
|
||||||
|
\hline
|
||||||
|
\textbf{Letzte Arbeitsaufnahme:} & {{letzte_arbeit|date}} \\
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.5em}
|
||||||
|
|
||||||
|
% === DIAGNOSES ===
|
||||||
|
\sectionheader{3. Diagnosen (mit ICD-10-Schlüssel)}
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\begin{tabularx}{\linewidth}{|p{1cm}|X|p{2.5cm}|}
|
||||||
|
\hline
|
||||||
|
\textbf{Nr.} & \textbf{Diagnose} & \textbf{ICD-10} \\
|
||||||
|
\hline
|
||||||
|
1. & {{diagnose_1}} & {{diagnose_1_icd}} \\
|
||||||
|
\hline
|
||||||
|
2. & {{diagnose_2}} & {{diagnose_2_icd}} \\
|
||||||
|
\hline
|
||||||
|
3. & {{diagnose_3}} & {{diagnose_3_icd}} \\
|
||||||
|
\hline
|
||||||
|
4. & {{diagnose_4}} & {{diagnose_4_icd}} \\
|
||||||
|
\hline
|
||||||
|
5. & {{diagnose_5}} & {{diagnose_5_icd}} \\
|
||||||
|
\hline
|
||||||
|
6. & {{diagnose_6}} & {{diagnose_6_icd}} \\
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.5em}
|
||||||
|
|
||||||
|
% === ANAMNESIS ===
|
||||||
|
\sectionheader{4. Anamnese und aktueller Befund}
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\textbf{4.1 Eigenanamnese / Beschwerden des Patienten:}
|
||||||
|
\begin{tabularx}{\linewidth}{|X|}
|
||||||
|
\hline
|
||||||
|
{{anamnese_beschwerden}} \\[4em]
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\textbf{4.2 Krankheitsverlauf / bisherige Behandlungen:}
|
||||||
|
\begin{tabularx}{\linewidth}{|X|}
|
||||||
|
\hline
|
||||||
|
{{krankheitsverlauf}} \\[4em]
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\textbf{4.3 Aktueller körperlicher Befund:}
|
||||||
|
\begin{tabularx}{\linewidth}{|X|}
|
||||||
|
\hline
|
||||||
|
{{koerperlicher_befund}} \\[4em]
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\newpage
|
||||||
|
|
||||||
|
% === FUNCTIONAL LIMITATIONS ===
|
||||||
|
\sectionheader{5. Nicht nur vorübergehende Beeinträchtigungen der Aktivitäten und Teilhabe}
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\begin{tabularx}{\linewidth}{|X|c|c|c|}
|
||||||
|
\hline
|
||||||
|
\textbf{Beeinträchtigungsbereich} & \textbf{Keine} & \textbf{Gering} & \textbf{Erheblich} \\
|
||||||
|
\hline
|
||||||
|
Mobilität (Gehen, Treppensteigen, Fortbewegung) & {{mobilitaet_keine|checkbox}} & {{mobilitaet_gering|checkbox}} & {{mobilitaet_erheblich|checkbox}} \\
|
||||||
|
\hline
|
||||||
|
Selbstversorgung (Waschen, Ankleiden, Essen) & {{selbstversorgung_keine|checkbox}} & {{selbstversorgung_gering|checkbox}} & {{selbstversorgung_erheblich|checkbox}} \\
|
||||||
|
\hline
|
||||||
|
Haushaltsführung & {{haushalt_keine|checkbox}} & {{haushalt_gering|checkbox}} & {{haushalt_erheblich|checkbox}} \\
|
||||||
|
\hline
|
||||||
|
Erwerbstätigkeit & {{erwerb_keine|checkbox}} & {{erwerb_gering|checkbox}} & {{erwerb_erheblich|checkbox}} \\
|
||||||
|
\hline
|
||||||
|
Kommunikation & {{kommunikation_keine|checkbox}} & {{kommunikation_gering|checkbox}} & {{kommunikation_erheblich|checkbox}} \\
|
||||||
|
\hline
|
||||||
|
Psychische Belastbarkeit & {{psyche_keine|checkbox}} & {{psyche_gering|checkbox}} & {{psyche_erheblich|checkbox}} \\
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\textbf{Erläuterungen zu den Beeinträchtigungen:}
|
||||||
|
\begin{tabularx}{\linewidth}{|X|}
|
||||||
|
\hline
|
||||||
|
{{beeintraechtigungen_erlaeuterung}} \\[3em]
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.5em}
|
||||||
|
|
||||||
|
% === THERAPY ===
|
||||||
|
\sectionheader{6. Bisherige und aktuelle Therapie}
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\textbf{6.1 Aktuelle Medikation:}
|
||||||
|
\begin{tabularx}{\linewidth}{|X|p{2cm}|p{2.5cm}|}
|
||||||
|
\hline
|
||||||
|
\textbf{Medikament} & \textbf{Dosis} & \textbf{Seit wann} \\
|
||||||
|
\hline
|
||||||
|
{{medikament_1}} & {{medikament_1_dosis}} & {{medikament_1_seit}} \\
|
||||||
|
\hline
|
||||||
|
{{medikament_2}} & {{medikament_2_dosis}} & {{medikament_2_seit}} \\
|
||||||
|
\hline
|
||||||
|
{{medikament_3}} & {{medikament_3_dosis}} & {{medikament_3_seit}} \\
|
||||||
|
\hline
|
||||||
|
{{medikament_4}} & {{medikament_4_dosis}} & {{medikament_4_seit}} \\
|
||||||
|
\hline
|
||||||
|
{{medikament_5}} & {{medikament_5_dosis}} & {{medikament_5_seit}} \\
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\textbf{6.2 Physikalische Therapie / Heilmittel:}
|
||||||
|
\begin{tabularx}{\linewidth}{|X|}
|
||||||
|
\hline
|
||||||
|
{{physikalische_therapie}} \\[2em]
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.5em}
|
||||||
|
|
||||||
|
% === PREVIOUS REHAB ===
|
||||||
|
\sectionheader{7. Frühere Rehabilitationsmaßnahmen}
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\begin{tabularx}{\linewidth}{|p{3cm}|X|p{2.5cm}|}
|
||||||
|
\hline
|
||||||
|
\textbf{Zeitraum} & \textbf{Einrichtung / Art} & \textbf{Erfolg} \\
|
||||||
|
\hline
|
||||||
|
{{reha_1_zeitraum}} & {{reha_1_einrichtung}} & {{reha_1_erfolg}} \\
|
||||||
|
\hline
|
||||||
|
{{reha_2_zeitraum}} & {{reha_2_einrichtung}} & {{reha_2_erfolg}} \\
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.5em}
|
||||||
|
|
||||||
|
% === ASSESSMENT ===
|
||||||
|
\sectionheader{8. Ärztliche Beurteilung}
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\textbf{8.1 Leistungsvermögen im bisherigen Beruf:}\\
|
||||||
|
{{leistungsvermoegen_checkbox_vollschichtig|checkbox}} Vollschichtig (6 Std. und mehr) \quad
|
||||||
|
{{leistungsvermoegen_checkbox_teilschichtig|checkbox}} 3-6 Stunden \quad
|
||||||
|
{{leistungsvermoegen_checkbox_unter3|checkbox}} Unter 3 Stunden
|
||||||
|
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\textbf{8.2 Rehabilitationsbedürftigkeit:}
|
||||||
|
\begin{tabularx}{\linewidth}{|X|}
|
||||||
|
\hline
|
||||||
|
{{reha_beduerftig_begruendung}} \\[3em]
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\textbf{8.3 Rehabilitationsziel:}
|
||||||
|
\begin{tabularx}{\linewidth}{|X|}
|
||||||
|
\hline
|
||||||
|
{{reha_ziel}} \\[2em]
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\textbf{8.4 Empfohlene Rehabilitationsform:}\\
|
||||||
|
{{reha_stationaer|checkbox}} Stationär \quad
|
||||||
|
{{reha_ambulant|checkbox}} Ambulant \quad
|
||||||
|
{{reha_ganztaegig|checkbox}} Ganztägig ambulant
|
||||||
|
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\textbf{8.5 Empfohlene Rehabilitationseinrichtung (falls bekannt):}
|
||||||
|
\begin{tabularx}{\linewidth}{|X|}
|
||||||
|
\hline
|
||||||
|
{{reha_einrichtung_empfehlung}} \\[1em]
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\newpage
|
||||||
|
|
||||||
|
% === TRAVEL CAPABILITY ===
|
||||||
|
\sectionheader{9. Reisefähigkeit}
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\textbf{Kann der Patient mit öffentlichen Verkehrsmitteln reisen?}\\
|
||||||
|
{{reisefaehig_ja|checkbox}} Ja \quad
|
||||||
|
{{reisefaehig_nein|checkbox}} Nein
|
||||||
|
|
||||||
|
\vspace{0.2em}
|
||||||
|
\textbf{Falls nein, Begründung:}
|
||||||
|
\begin{tabularx}{\linewidth}{|X|}
|
||||||
|
\hline
|
||||||
|
{{reisefaehig_begruendung}} \\[2em]
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\textbf{Begleitperson erforderlich?}\\
|
||||||
|
{{begleitperson_ja|checkbox}} Ja \quad
|
||||||
|
{{begleitperson_nein|checkbox}} Nein
|
||||||
|
|
||||||
|
\vspace{0.5em}
|
||||||
|
|
||||||
|
% === ADDITIONAL INFO ===
|
||||||
|
\sectionheader{10. Ergänzende Angaben}
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
\begin{tabularx}{\linewidth}{|X|}
|
||||||
|
\hline
|
||||||
|
{{ergaenzende_angaben}} \\[4em]
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{0.5em}
|
||||||
|
|
||||||
|
% === ATTACHMENTS ===
|
||||||
|
\sectionheader{11. Anlagen}
|
||||||
|
\vspace{0.3em}
|
||||||
|
|
||||||
|
{{anlage_laborbefunde|checkbox}} Laborbefunde \quad
|
||||||
|
{{anlage_roentgen|checkbox}} Röntgen-/Bildgebende Befunde \quad
|
||||||
|
{{anlage_arztbriefe|checkbox}} Arztbriefe\\
|
||||||
|
{{anlage_krankenhausberichte|checkbox}} Krankenhausberichte \quad
|
||||||
|
{{anlage_sonstige|checkbox}} Sonstige: {{anlage_sonstige_text}}
|
||||||
|
|
||||||
|
\vspace{1em}
|
||||||
|
|
||||||
|
% === SIGNATURE ===
|
||||||
|
\sectionheader{12. Unterschrift des behandelnden Arztes}
|
||||||
|
\vspace{0.5em}
|
||||||
|
|
||||||
|
\begin{tabularx}{\linewidth}{|p{5cm}|X|}
|
||||||
|
\hline
|
||||||
|
\textbf{Datum:} & {{unterschrift_datum|date}} \\
|
||||||
|
\hline
|
||||||
|
\textbf{Name des Arztes:} & {{arzt_name}} \\
|
||||||
|
\hline
|
||||||
|
\textbf{Facharztbezeichnung:} & {{arzt_fachrichtung}} \\
|
||||||
|
\hline
|
||||||
|
\textbf{Anschrift Praxis:} & {{praxis_anschrift}} \\
|
||||||
|
\hline
|
||||||
|
\textbf{Telefon:} & {{praxis_telefon}} \\
|
||||||
|
\hline
|
||||||
|
\textbf{BSNR/LANR:} & {{bsnr}} / {{lanr}} \\
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{1.5em}
|
||||||
|
|
||||||
|
\begin{tabularx}{\linewidth}{X X}
|
||||||
|
\hrulefill & \hrulefill \\
|
||||||
|
Ort, Datum & Unterschrift und Stempel \\
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
\vspace{1em}
|
||||||
|
|
||||||
|
\footnotesize
|
||||||
|
\textit{Hinweis: Die Angaben werden vertraulich behandelt und unterliegen dem Sozialgeheimnis nach § 35 SGB I.}
|
||||||
|
|
||||||
|
\end{document}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue