Phase 8: Customizable PDF Templates — locale support, admin editor, seed templates
Backend: - PdfTemplate model with locale field + UNIQUE(name, locale) constraint - Migration 007: pdf_templates table + template_id FK on generated_pdfs - Template service: CRUD, Jinja2 validation, render preview with sample data - Admin endpoints: CRUD /admin/pdf-templates + POST preview - User endpoint: GET /pdf/templates (active templates list) - pdf_service: resolves template from DB by ID or falls back to default for the appropriate locale - AI generate_pdf tool accepts optional template_id - Seed script + 4 HTML template files: - Basic Report (en/ru) — general-purpose report - Medical Report (en/ru) — health-focused with disclaimers Frontend: - Admin PDF templates page with editor, locale selector, live preview (iframe), template variables reference panel - PDF page: template selector dropdown in generation form - API clients for admin CRUD + user template listing - Sidebar: admin templates link - English + Russian translations Also added Phase 9 (OAuth) and Phase 10 (Rate Limits) placeholders to GeneralPlan. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
70
backend/app/templates/pdf/seeds/basic_en.html
Normal file
70
backend/app/templates/pdf/seeds/basic_en.html
Normal file
@@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: 'Helvetica', 'Arial', sans-serif; font-size: 12pt; color: #333; margin: 40px; }
|
||||
h1 { color: #1a7a8a; border-bottom: 2px solid #1a7a8a; padding-bottom: 8px; }
|
||||
h2 { color: #2a5a6a; margin-top: 24px; }
|
||||
.header { margin-bottom: 24px; }
|
||||
.header p { margin: 2px 0; color: #666; }
|
||||
.section { margin-bottom: 20px; }
|
||||
.memory-entry { background: #f5f9fa; padding: 10px 14px; border-radius: 6px; margin-bottom: 8px; }
|
||||
.memory-entry .category { font-size: 10pt; color: #888; text-transform: uppercase; }
|
||||
.memory-entry .title { font-weight: bold; }
|
||||
.memory-entry .importance { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 9pt; }
|
||||
.importance-critical { background: #fee; color: #c33; }
|
||||
.importance-high { background: #fff3e0; color: #e65100; }
|
||||
.importance-medium { background: #e3f2fd; color: #1565c0; }
|
||||
.importance-low { background: #f5f5f5; color: #666; }
|
||||
.document { border: 1px solid #ddd; padding: 12px; border-radius: 6px; margin-bottom: 10px; }
|
||||
.document .filename { font-weight: bold; color: #1a7a8a; }
|
||||
.document .excerpt { font-size: 10pt; color: #555; white-space: pre-wrap; max-height: 200px; overflow: hidden; }
|
||||
.footer { margin-top: 40px; padding-top: 12px; border-top: 1px solid #ddd; font-size: 9pt; color: #999; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>{{ title }}</h1>
|
||||
<p>Prepared for: {{ user_name }}</p>
|
||||
<p>Generated: {{ generated_at }}</p>
|
||||
</div>
|
||||
|
||||
{% if memories %}
|
||||
<div class="section">
|
||||
<h2>Key Information</h2>
|
||||
{% for m in memories %}
|
||||
<div class="memory-entry">
|
||||
<span class="category">{{ m.category }}</span>
|
||||
<span class="importance importance-{{ m.importance }}">{{ m.importance }}</span>
|
||||
<div class="title">{{ m.title }}</div>
|
||||
<div>{{ m.content }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if documents %}
|
||||
<div class="section">
|
||||
<h2>Document Summaries</h2>
|
||||
{% for d in documents %}
|
||||
<div class="document">
|
||||
<div class="filename">{{ d.original_filename }} ({{ d.doc_type }})</div>
|
||||
<div class="excerpt">{{ d.excerpt }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ai_summary %}
|
||||
<div class="section">
|
||||
<h2>AI Summary</h2>
|
||||
<p>{{ ai_summary }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="footer">
|
||||
Generated by AI Assistant • This document is for informational purposes only.
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
70
backend/app/templates/pdf/seeds/basic_ru.html
Normal file
70
backend/app/templates/pdf/seeds/basic_ru.html
Normal file
@@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: 'Helvetica', 'Arial', sans-serif; font-size: 12pt; color: #333; margin: 40px; }
|
||||
h1 { color: #1a7a8a; border-bottom: 2px solid #1a7a8a; padding-bottom: 8px; }
|
||||
h2 { color: #2a5a6a; margin-top: 24px; }
|
||||
.header { margin-bottom: 24px; }
|
||||
.header p { margin: 2px 0; color: #666; }
|
||||
.section { margin-bottom: 20px; }
|
||||
.entry { background: #f5f9fa; padding: 10px 14px; border-radius: 6px; margin-bottom: 8px; }
|
||||
.entry .cat { font-size: 10pt; color: #888; text-transform: uppercase; }
|
||||
.entry .title { font-weight: bold; }
|
||||
.entry .badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 9pt; }
|
||||
.badge-critical { background: #fee; color: #c33; }
|
||||
.badge-high { background: #fff3e0; color: #e65100; }
|
||||
.badge-medium { background: #e3f2fd; color: #1565c0; }
|
||||
.badge-low { background: #f5f5f5; color: #666; }
|
||||
.doc { border: 1px solid #ddd; padding: 12px; border-radius: 6px; margin-bottom: 10px; }
|
||||
.doc .fname { font-weight: bold; color: #1a7a8a; }
|
||||
.doc .excerpt { font-size: 10pt; color: #555; white-space: pre-wrap; max-height: 200px; overflow: hidden; }
|
||||
.footer { margin-top: 40px; padding-top: 12px; border-top: 1px solid #ddd; font-size: 9pt; color: #999; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>{{ title }}</h1>
|
||||
<p>Подготовлено для: {{ user_name }}</p>
|
||||
<p>Дата: {{ generated_at }}</p>
|
||||
</div>
|
||||
|
||||
{% if memories %}
|
||||
<div class="section">
|
||||
<h2>Ключевая информация</h2>
|
||||
{% for m in memories %}
|
||||
<div class="entry">
|
||||
<span class="cat">{{ m.category }}</span>
|
||||
<span class="badge badge-{{ m.importance }}">{{ m.importance }}</span>
|
||||
<div class="title">{{ m.title }}</div>
|
||||
<div>{{ m.content }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if documents %}
|
||||
<div class="section">
|
||||
<h2>Сводка по документам</h2>
|
||||
{% for d in documents %}
|
||||
<div class="doc">
|
||||
<div class="fname">{{ d.original_filename }} ({{ d.doc_type }})</div>
|
||||
<div class="excerpt">{{ d.excerpt }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ai_summary %}
|
||||
<div class="section">
|
||||
<h2>Резюме ИИ</h2>
|
||||
<p>{{ ai_summary }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="footer">
|
||||
Сгенерировано ИИ-ассистентом
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
75
backend/app/templates/pdf/seeds/medical_en.html
Normal file
75
backend/app/templates/pdf/seeds/medical_en.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: 'Georgia', serif; font-size: 11pt; color: #222; margin: 30px; }
|
||||
h1 { color: #c0392b; border-bottom: 3px double #c0392b; padding-bottom: 10px; }
|
||||
h2 { color: #8e2323; margin-top: 20px; border-left: 4px solid #c0392b; padding-left: 10px; }
|
||||
.header { margin-bottom: 20px; }
|
||||
.header p { margin: 2px 0; color: #555; }
|
||||
.warning { background: #fff3cd; border: 1px solid #ffc107; padding: 10px; border-radius: 4px; margin-bottom: 16px; font-size: 10pt; }
|
||||
.section { margin-bottom: 16px; }
|
||||
.entry { border-bottom: 1px solid #eee; padding: 8px 0; }
|
||||
.entry .cat { font-size: 9pt; color: #999; text-transform: uppercase; letter-spacing: 1px; }
|
||||
.entry .title { font-weight: bold; font-size: 12pt; }
|
||||
.entry .badge { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 8pt; font-weight: bold; }
|
||||
.badge-critical { background: #e74c3c; color: white; }
|
||||
.badge-high { background: #e67e22; color: white; }
|
||||
.badge-medium { background: #3498db; color: white; }
|
||||
.badge-low { background: #95a5a6; color: white; }
|
||||
.doc { background: #f8f9fa; padding: 10px; margin-bottom: 8px; border-left: 3px solid #3498db; }
|
||||
.doc .fname { font-weight: bold; }
|
||||
.doc .excerpt { font-size: 10pt; color: #444; white-space: pre-wrap; }
|
||||
.footer { margin-top: 30px; padding-top: 10px; border-top: 2px solid #eee; font-size: 8pt; color: #999; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Medical Report: {{ title }}</h1>
|
||||
<p>Patient: {{ user_name }}</p>
|
||||
<p>Date: {{ generated_at }}</p>
|
||||
</div>
|
||||
|
||||
<div class="warning">
|
||||
This report is generated by AI and is for informational purposes only. Always consult a healthcare professional for medical decisions.
|
||||
</div>
|
||||
|
||||
{% if memories %}
|
||||
<div class="section">
|
||||
<h2>Health Profile</h2>
|
||||
{% for m in memories %}
|
||||
<div class="entry">
|
||||
<span class="cat">{{ m.category }}</span>
|
||||
<span class="badge badge-{{ m.importance }}">{{ m.importance }}</span>
|
||||
<div class="title">{{ m.title }}</div>
|
||||
<div>{{ m.content }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if documents %}
|
||||
<div class="section">
|
||||
<h2>Medical Documents</h2>
|
||||
{% for d in documents %}
|
||||
<div class="doc">
|
||||
<div class="fname">{{ d.original_filename }} ({{ d.doc_type }})</div>
|
||||
<div class="excerpt">{{ d.excerpt }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ai_summary %}
|
||||
<div class="section">
|
||||
<h2>AI Clinical Summary</h2>
|
||||
<p>{{ ai_summary }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="footer">
|
||||
Generated by AI Assistant • For informational purposes only • Not a substitute for professional medical advice
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
75
backend/app/templates/pdf/seeds/medical_ru.html
Normal file
75
backend/app/templates/pdf/seeds/medical_ru.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: 'Georgia', serif; font-size: 11pt; color: #222; margin: 30px; }
|
||||
h1 { color: #c0392b; border-bottom: 3px double #c0392b; padding-bottom: 10px; }
|
||||
h2 { color: #8e2323; margin-top: 20px; border-left: 4px solid #c0392b; padding-left: 10px; }
|
||||
.header { margin-bottom: 20px; }
|
||||
.header p { margin: 2px 0; color: #555; }
|
||||
.warning { background: #fff3cd; border: 1px solid #ffc107; padding: 10px; border-radius: 4px; margin-bottom: 16px; font-size: 10pt; }
|
||||
.section { margin-bottom: 16px; }
|
||||
.entry { border-bottom: 1px solid #eee; padding: 8px 0; }
|
||||
.entry .cat { font-size: 9pt; color: #999; text-transform: uppercase; letter-spacing: 1px; }
|
||||
.entry .title { font-weight: bold; font-size: 12pt; }
|
||||
.entry .badge { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 8pt; font-weight: bold; }
|
||||
.badge-critical { background: #e74c3c; color: white; }
|
||||
.badge-high { background: #e67e22; color: white; }
|
||||
.badge-medium { background: #3498db; color: white; }
|
||||
.badge-low { background: #95a5a6; color: white; }
|
||||
.doc { background: #f8f9fa; padding: 10px; margin-bottom: 8px; border-left: 3px solid #3498db; }
|
||||
.doc .fname { font-weight: bold; }
|
||||
.doc .excerpt { font-size: 10pt; color: #444; white-space: pre-wrap; }
|
||||
.footer { margin-top: 30px; padding-top: 10px; border-top: 2px solid #eee; font-size: 8pt; color: #999; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Медицинский отчёт: {{ title }}</h1>
|
||||
<p>Пациент: {{ user_name }}</p>
|
||||
<p>Дата: {{ generated_at }}</p>
|
||||
</div>
|
||||
|
||||
<div class="warning">
|
||||
Этот отчёт сгенерирован ИИ и предназначен только для информационных целей. Всегда консультируйтесь с врачом для принятия медицинских решений.
|
||||
</div>
|
||||
|
||||
{% if memories %}
|
||||
<div class="section">
|
||||
<h2>Профиль здоровья</h2>
|
||||
{% for m in memories %}
|
||||
<div class="entry">
|
||||
<span class="cat">{{ m.category }}</span>
|
||||
<span class="badge badge-{{ m.importance }}">{{ m.importance }}</span>
|
||||
<div class="title">{{ m.title }}</div>
|
||||
<div>{{ m.content }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if documents %}
|
||||
<div class="section">
|
||||
<h2>Медицинские документы</h2>
|
||||
{% for d in documents %}
|
||||
<div class="doc">
|
||||
<div class="fname">{{ d.original_filename }} ({{ d.doc_type }})</div>
|
||||
<div class="excerpt">{{ d.excerpt }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ai_summary %}
|
||||
<div class="section">
|
||||
<h2>Клиническое резюме ИИ</h2>
|
||||
<p>{{ ai_summary }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="footer">
|
||||
Сгенерировано ИИ-ассистентом • Только для информационных целей • Не заменяет консультацию врача
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user