feat: библиотека шаблонов документов (25 шт.) в стиле REFLY
- DocumentTemplate: модель, CRUD API, seed 25 шаблонов - Категории: заявки, сертификаты, акты, письма, формы, отчёты, приказы - Стандарты: РФ (ФАП-145/146/148/246), ICAO, EASA, FAA - Бренд-бук REFLY: шапка, подвал, цвета #1e3a5f, печатный формат A4 - Поля contenteditable для заполнения в браузере - DocumentPreviewModal: просмотр, заполнение, печать - Страница /templates с фильтрами по категории и стандарту - Alembic миграция 0002_document_templates Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
44b14cc4fd
commit
0cf1cfdaec
@ -9,6 +9,7 @@ export default function DocumentsPage() {
|
||||
{ title: 'Сертификаты', desc: 'Сертификаты ЛГ', href: '/airworthiness', icon: '📜' },
|
||||
{ title: 'Нормативные документы', desc: 'ФАП, ICAO, EASA', href: '/regulations', icon: '📚' },
|
||||
{ title: 'Чек-листы', desc: 'Шаблоны проверок', href: '/checklists', icon: '✅' },
|
||||
{ title: 'Шаблоны документов', desc: 'Заявки, акты, письма, формы', href: '/templates', icon: '📋' },
|
||||
];
|
||||
return (
|
||||
<PageLayout title="Документы" subtitle="Просмотр документов, прикреплённых к ВС, аудитам и заявкам">
|
||||
|
||||
105
app/templates/page.tsx
Normal file
105
app/templates/page.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { PageLayout, FilterBar, EmptyState } from '@/components/ui';
|
||||
import { documentTemplatesApi } from '@/lib/api/api-client';
|
||||
import useSWR from 'swr';
|
||||
import DocumentPreviewModal from '@/components/DocumentPreviewModal';
|
||||
|
||||
const CATEGORIES = [
|
||||
{ value: undefined, label: 'Все' },
|
||||
{ value: 'application', label: '📝 Заявки' },
|
||||
{ value: 'certificate', label: '📜 Сертификаты' },
|
||||
{ value: 'act', label: '📋 Акты' },
|
||||
{ value: 'letter', label: '✉️ Письма' },
|
||||
{ value: 'form', label: '📄 Формы' },
|
||||
{ value: 'report', label: '📊 Отчёты' },
|
||||
{ value: 'order', label: '📌 Приказы' },
|
||||
];
|
||||
|
||||
const STANDARDS = [
|
||||
{ value: undefined, label: 'Все' },
|
||||
{ value: 'RF', label: '🇷🇺 РФ' },
|
||||
{ value: 'ICAO', label: '🌐 ИКАО' },
|
||||
{ value: 'EASA', label: '🇪🇺 EASA' },
|
||||
{ value: 'FAA', label: '🇺🇸 FAA' },
|
||||
{ value: 'INTERNAL', label: '🏢 Внутренние' },
|
||||
];
|
||||
|
||||
const CATEGORY_ICONS: Record<string, string> = {
|
||||
application: '📝',
|
||||
certificate: '📜',
|
||||
act: '📋',
|
||||
letter: '✉️',
|
||||
form: '📄',
|
||||
report: '📊',
|
||||
order: '📌',
|
||||
};
|
||||
|
||||
export default function TemplatesPage() {
|
||||
const [category, setCategory] = useState<string | undefined>();
|
||||
const [standard, setStandard] = useState<string | undefined>();
|
||||
const [preview, setPreview] = useState<any>(null);
|
||||
|
||||
const params: Record<string, string | undefined> = {};
|
||||
if (category) params.category = category;
|
||||
if (standard) params.standard = standard;
|
||||
|
||||
const { data, isLoading, mutate } = useSWR(
|
||||
['doc-templates', category, standard],
|
||||
() => documentTemplatesApi.list(params)
|
||||
);
|
||||
|
||||
const templates = data?.items || [];
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title="Шаблоны документов"
|
||||
subtitle={isLoading ? 'Загрузка...' : `${data?.total || 0} шаблонов`}
|
||||
>
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
<FilterBar value={category} onChange={setCategory} options={CATEGORIES} className="mb-0" />
|
||||
<FilterBar value={standard} onChange={setStandard} options={STANDARDS} className="mb-0" />
|
||||
</div>
|
||||
|
||||
{!isLoading && templates.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{templates.map((t: any) => (
|
||||
<div
|
||||
key={t.id}
|
||||
onClick={() => setPreview(t)}
|
||||
className="group cursor-pointer rounded-lg border border-gray-200 bg-white p-5 shadow-sm transition-all hover:border-primary-300 hover:shadow-md"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-2xl">{CATEGORY_ICONS[t.category] || '📄'}</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-bold text-primary-500 group-hover:underline">{t.name}</div>
|
||||
<div className="mt-1 text-xs text-gray-500">
|
||||
{t.code} · v{t.version}
|
||||
</div>
|
||||
{t.description && (
|
||||
<div className="mt-2 line-clamp-2 text-xs text-gray-400">{t.description}</div>
|
||||
)}
|
||||
<div className="mt-2 flex gap-2">
|
||||
<span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700">{t.standard}</span>
|
||||
<span className="rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-600">{t.category}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : !isLoading ? (
|
||||
<EmptyState message="Нет шаблонов по выбранным фильтрам." />
|
||||
) : null}
|
||||
|
||||
{preview && (
|
||||
<DocumentPreviewModal
|
||||
template={preview}
|
||||
onClose={() => setPreview(null)}
|
||||
onSaved={() => mutate()}
|
||||
/>
|
||||
)}
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
39
backend/alembic/versions/0002_document_templates.py
Normal file
39
backend/alembic/versions/0002_document_templates.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""Document templates table
|
||||
|
||||
Revision ID: 0002
|
||||
Revises: 0001
|
||||
Create Date: 2026-02-16
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '0002'
|
||||
down_revision = '0001'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
'document_templates',
|
||||
sa.Column('id', sa.String(36), primary_key=True),
|
||||
sa.Column('code', sa.String(32), nullable=False),
|
||||
sa.Column('name', sa.String(255), nullable=False),
|
||||
sa.Column('category', sa.String(64), nullable=False),
|
||||
sa.Column('standard', sa.String(32), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('html_content', sa.Text(), nullable=False),
|
||||
sa.Column('version', sa.Integer(), nullable=False, server_default='1'),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('sort_order', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index('idx_document_templates_code', 'document_templates', ['code'], unique=True)
|
||||
op.create_index('idx_document_templates_category', 'document_templates', ['category'])
|
||||
op.create_index('idx_document_templates_standard', 'document_templates', ['standard'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table('document_templates')
|
||||
@ -17,6 +17,7 @@ from .inbox import router as inbox_router
|
||||
from .tasks import router as tasks_router
|
||||
from .audit import router as audit_router
|
||||
from .ai import router as ai_router
|
||||
from .document_templates import router as document_templates_router
|
||||
|
||||
__all__ = [
|
||||
"health_router",
|
||||
@ -38,5 +39,6 @@ __all__ = [
|
||||
"tasks_router",
|
||||
"audit_router",
|
||||
"ai_router",
|
||||
"document_templates_router",
|
||||
]
|
||||
|
||||
|
||||
78
backend/app/api/routes/document_templates.py
Normal file
78
backend/app/api/routes/document_templates.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""Шаблоны документов — CRUD + предпросмотр."""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.api.deps import get_current_user, require_roles, get_db
|
||||
from app.api.helpers import paginate_query
|
||||
from app.models.document_template import DocumentTemplate
|
||||
|
||||
router = APIRouter(tags=["document_templates"])
|
||||
|
||||
|
||||
class TemplateOut(BaseModel):
|
||||
id: str
|
||||
code: str
|
||||
name: str
|
||||
category: str
|
||||
standard: str
|
||||
description: str | None
|
||||
html_content: str
|
||||
version: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TemplateUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
html_content: str | None = None
|
||||
|
||||
|
||||
@router.get("/document-templates")
|
||||
def list_templates(
|
||||
category: str | None = None,
|
||||
standard: str | None = None,
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
q = db.query(DocumentTemplate).filter(DocumentTemplate.is_active == True)
|
||||
if category:
|
||||
q = q.filter(DocumentTemplate.category == category)
|
||||
if standard:
|
||||
q = q.filter(DocumentTemplate.standard == standard)
|
||||
q = q.order_by(DocumentTemplate.sort_order, DocumentTemplate.name)
|
||||
return paginate_query(q, page, per_page)
|
||||
|
||||
|
||||
@router.get("/document-templates/{template_id}", response_model=TemplateOut)
|
||||
def get_template(template_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
||||
t = db.query(DocumentTemplate).filter(DocumentTemplate.id == template_id).first()
|
||||
if not t:
|
||||
raise HTTPException(404, "Template not found")
|
||||
return t
|
||||
|
||||
|
||||
@router.patch("/document-templates/{template_id}", response_model=TemplateOut,
|
||||
dependencies=[Depends(require_roles("admin", "authority_inspector"))])
|
||||
def update_template(
|
||||
template_id: str,
|
||||
payload: TemplateUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
t = db.query(DocumentTemplate).filter(DocumentTemplate.id == template_id).first()
|
||||
if not t:
|
||||
raise HTTPException(404, "Template not found")
|
||||
if payload.name is not None:
|
||||
t.name = payload.name
|
||||
if payload.description is not None:
|
||||
t.description = payload.description
|
||||
if payload.html_content is not None:
|
||||
t.html_content = payload.html_content
|
||||
db.commit()
|
||||
db.refresh(t)
|
||||
return t
|
||||
@ -224,6 +224,8 @@ def seed():
|
||||
|
||||
from app.demo.seed_checklists import seed_checklists
|
||||
seed_checklists()
|
||||
from app.demo.seed_document_templates import seed_document_templates
|
||||
seed_document_templates()
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
|
||||
570
backend/app/demo/seed_document_templates.py
Normal file
570
backend/app/demo/seed_document_templates.py
Normal file
@ -0,0 +1,570 @@
|
||||
"""
|
||||
Seed 25 шаблонов документов авиационной отрасли в стиле REFLY.
|
||||
"""
|
||||
import logging
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.document_template import DocumentTemplate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_STYLE = """
|
||||
<style>
|
||||
.doc { font-family: 'Inter', -apple-system, sans-serif; max-width: 210mm; margin: 0 auto; padding: 20mm; color: #333; font-size: 12pt; line-height: 1.5; }
|
||||
.header { text-align: center; border-bottom: 3px solid #1e3a5f; padding-bottom: 12px; margin-bottom: 20px; }
|
||||
.logo { font-size: 28pt; font-weight: 900; color: #1e3a5f; letter-spacing: 3px; }
|
||||
.org-name { font-size: 11pt; color: #1e3a5f; font-weight: 600; margin-top: 4px; }
|
||||
.org-details { font-size: 8pt; color: #888; margin-top: 2px; }
|
||||
.doc-title { text-align: center; font-size: 16pt; font-weight: 700; margin: 24px 0 8px; text-transform: uppercase; }
|
||||
.doc-subtitle { text-align: center; font-size: 10pt; color: #666; margin-bottom: 20px; }
|
||||
.doc-number { text-align: right; font-size: 10pt; color: #666; margin-bottom: 16px; }
|
||||
.field { background: #f0f4ff; border-bottom: 1px solid #1e3a5f; padding: 2px 8px; min-width: 120px; display: inline-block; cursor: text; }
|
||||
.field:focus { background: #e0e8ff; outline: 2px solid #4a90e2; }
|
||||
.section-title { font-size: 12pt; font-weight: 700; color: #1e3a5f; margin: 16px 0 8px; border-bottom: 1px solid #ddd; padding-bottom: 4px; }
|
||||
table.doc-table { width: 100%; border-collapse: collapse; margin: 12px 0; }
|
||||
table.doc-table th { background: #1e3a5f; color: white; padding: 8px 12px; text-align: left; font-size: 10pt; }
|
||||
table.doc-table td { padding: 8px 12px; border: 1px solid #ddd; font-size: 10pt; }
|
||||
table.doc-table tr:nth-child(even) { background: #f8fafc; }
|
||||
.signatures { display: flex; justify-content: space-between; margin-top: 40px; }
|
||||
.sig-block { width: 45%; }
|
||||
.sig-line { border-bottom: 1px solid #333; margin-top: 40px; padding-top: 4px; font-size: 10pt; }
|
||||
.sig-label { font-size: 8pt; color: #888; }
|
||||
.footer { text-align: center; font-size: 8pt; color: #aaa; margin-top: 40px; padding-top: 12px; border-top: 1px solid #ddd; }
|
||||
.stamp-area { border: 1px dashed #ccc; width: 100px; height: 100px; display: inline-block; text-align: center; line-height: 100px; font-size: 8pt; color: #ccc; }
|
||||
@media print { .doc { padding: 15mm; } .field { background: transparent; border-bottom: 1px solid #333; } }
|
||||
</style>
|
||||
"""
|
||||
|
||||
_HEADER = """
|
||||
<div class="header">
|
||||
<div class="logo">REFLY</div>
|
||||
<div class="org-name">АСУ ТК КЛГ — Контроль лётной годности</div>
|
||||
<div class="org-details">АО «REFLY» · г. Калининград · тел. +7 (4012) ХХХ-ХХ-ХХ · info@refly.ru</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
_FOOTER = """
|
||||
<div class="footer">АО «REFLY» · АСУ ТК КЛГ · Стр. ___ из ___</div>
|
||||
"""
|
||||
|
||||
|
||||
def _wrap(title: str, subtitle: str | None, content: str) -> str:
|
||||
sub = f'<div class="doc-subtitle">{subtitle}</div>' if subtitle else ""
|
||||
return (
|
||||
_STYLE
|
||||
+ '<div class="doc">'
|
||||
+ _HEADER
|
||||
+ f'<div class="doc-title">{title}</div>'
|
||||
+ sub
|
||||
+ '<div class="doc-number">№ <span class="field" contenteditable="true">[___________]</span> от <span class="field" contenteditable="true">[дата]</span></div>'
|
||||
+ content
|
||||
+ _FOOTER
|
||||
+ "</div>"
|
||||
)
|
||||
|
||||
|
||||
def _templates_data() -> list[dict]:
|
||||
return [
|
||||
{
|
||||
"code": "APP-246",
|
||||
"name": "Заявка на получение сертификата эксплуатанта (ФАП-246)",
|
||||
"category": "application",
|
||||
"standard": "RF",
|
||||
"description": "Заявка на получение сертификата эксплуатанта ВК РФ",
|
||||
"sort_order": 1,
|
||||
"html_content": _wrap(
|
||||
"Заявка на получение сертификата эксплуатанта",
|
||||
"ФАП-246",
|
||||
"""
|
||||
<p>Наименование организации: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>ИНН: <span class="field" contenteditable="true">[___]</span> ОГРН: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Адрес: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Типы ВС: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Виды авиаработ: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Ответственное лицо: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="signatures"><div class="sig-block"><div class="sig-line">Подпись руководителя</div><div class="sig-label">ФИО</div></div><div class="stamp-area">М.П.</div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "APP-145",
|
||||
"name": "Заявка на одобрение организации ТОиР (ФАП-145)",
|
||||
"category": "application",
|
||||
"standard": "RF",
|
||||
"description": "Заявка на одобрение организации по техническому обслуживанию",
|
||||
"sort_order": 2,
|
||||
"html_content": _wrap(
|
||||
"Заявка на одобрение организации ТОиР",
|
||||
"ФАП-145",
|
||||
"""
|
||||
<p>Наименование: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Область одобрения: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Типы ВС: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Категории работ: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="signatures"><div class="sig-block"><div class="sig-line">Подпись</div></div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "APP-SLG",
|
||||
"name": "Заявка на выдачу/продление СЛГ",
|
||||
"category": "application",
|
||||
"standard": "RF",
|
||||
"description": "Заявка на выдачу или продление сертификата лётной годности",
|
||||
"sort_order": 3,
|
||||
"html_content": _wrap(
|
||||
"Заявка на выдачу/продление сертификата лётной годности",
|
||||
None,
|
||||
"""
|
||||
<p>Рег. номер ВС: <span class="field" contenteditable="true">[___]</span> Тип: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Серийный №: <span class="field" contenteditable="true">[___]</span> Собственник: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Дата последнего ТО: <span class="field" contenteditable="true">[дата]</span></p>
|
||||
<div class="signatures"><div class="sig-block"><div class="sig-line">Подпись</div></div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "APP-MOD",
|
||||
"name": "Заявка на одобрение модификации ВС",
|
||||
"category": "application",
|
||||
"standard": "RF",
|
||||
"description": "Заявка на одобрение модификации воздушного судна",
|
||||
"sort_order": 4,
|
||||
"html_content": _wrap(
|
||||
"Заявка на одобрение модификации ВС",
|
||||
None,
|
||||
"""
|
||||
<p>Описание модификации: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>STC: <span class="field" contenteditable="true">[___]</span> Основание: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>ВС (рег. №): <span class="field" contenteditable="true">[___]</span></p>
|
||||
<div class="signatures"><div class="sig-block"><div class="sig-line">Подпись</div></div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "CERT-SLG",
|
||||
"name": "Сертификат лётной годности (СЛГ) — бланк",
|
||||
"category": "certificate",
|
||||
"standard": "RF",
|
||||
"description": "Бланк сертификата лётной годности",
|
||||
"sort_order": 5,
|
||||
"html_content": _wrap(
|
||||
"Сертификат лётной годности",
|
||||
None,
|
||||
"""
|
||||
<p>Государство регистрации: <span class="field" contenteditable="true">[___]</span> Рег. знак: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Тип: <span class="field" contenteditable="true">[___]</span> Серийный №: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Категория: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Дата выдачи: <span class="field" contenteditable="true">[дата]</span> Срок действия: <span class="field" contenteditable="true">[дата]</span></p>
|
||||
<p>Орган выдачи: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="stamp-area">Печать</div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "CERT-CRS",
|
||||
"name": "Свидетельство о допуске к эксплуатации (CRS)",
|
||||
"category": "certificate",
|
||||
"standard": "RF",
|
||||
"description": "ФАП-145 / EASA Part-145.A.50 — Certificate of Release to Service",
|
||||
"sort_order": 6,
|
||||
"html_content": _wrap(
|
||||
"Свидетельство о допуске к эксплуатации",
|
||||
"ФАП-145 / EASA Part-145.A.50",
|
||||
"""
|
||||
<p>Организация ТОиР: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>ВС: <span class="field" contenteditable="true">[рег. №]</span> Тип: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Выполненные работы: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Наряд №: <span class="field" contenteditable="true">[___]</span> Дата: <span class="field" contenteditable="true">[дата]</span></p>
|
||||
<p>Подпись уполномоченного лица: _____________________</p>
|
||||
<div class="stamp-area">М.П.</div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "CERT-EASA1",
|
||||
"name": "EASA Form 1 — Authorized Release Certificate",
|
||||
"category": "certificate",
|
||||
"standard": "EASA",
|
||||
"description": "Форма 1 EASA — сертификат авторизованного выпуска",
|
||||
"sort_order": 7,
|
||||
"html_content": _wrap(
|
||||
"EASA Form 1 — Authorized Release Certificate",
|
||||
"EASA",
|
||||
"""
|
||||
<table class="doc-table">
|
||||
<tr><th>Part Number</th><th>Description</th><th>Quantity</th><th>Serial No</th><th>Status</th><th>Remarks</th></tr>
|
||||
<tr><td><span class="field" contenteditable="true">[___]</span></td><td><span class="field" contenteditable="true">[___]</span></td><td><span class="field" contenteditable="true">[___]</span></td><td><span class="field" contenteditable="true">[___]</span></td><td><span class="field" contenteditable="true">[___]</span></td><td><span class="field" contenteditable="true">[___]</span></td></tr>
|
||||
</table>
|
||||
<p>Authorized Signature: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "CERT-8130",
|
||||
"name": "FAA Form 8130-3 — Airworthiness Approval Tag",
|
||||
"category": "certificate",
|
||||
"standard": "FAA",
|
||||
"description": "Форма 8130-3 FAA — ярлык одобрения лётной годности",
|
||||
"sort_order": 8,
|
||||
"html_content": _wrap(
|
||||
"FAA Form 8130-3 — Airworthiness Approval Tag",
|
||||
"FAA",
|
||||
"""
|
||||
<p>Part No: <span class="field" contenteditable="true">[___]</span> Serial No: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Description: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Status: <span class="field" contenteditable="true">[___]</span> Remarks: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Approved by: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "ACT-INSP",
|
||||
"name": "Акт инспекционной проверки ВС",
|
||||
"category": "act",
|
||||
"standard": "RF",
|
||||
"description": "Акт инспекционной проверки воздушного судна",
|
||||
"sort_order": 9,
|
||||
"html_content": _wrap(
|
||||
"Акт инспекционной проверки ВС",
|
||||
None,
|
||||
"""
|
||||
<p>Дата: <span class="field" contenteditable="true">[дата]</span> Место: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Борт: <span class="field" contenteditable="true">[рег. №]</span> Инспектор: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Результат: <span class="field" contenteditable="true">[годен / не годен]</span></p>
|
||||
<p>Замечания: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="signatures"><div class="sig-block"><div class="sig-line">Инспектор</div></div><div class="sig-block"><div class="sig-line">Представитель организации</div></div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "ACT-AUDIT",
|
||||
"name": "Акт аудита организации",
|
||||
"category": "act",
|
||||
"standard": "RF",
|
||||
"description": "Акт проведения аудита организации",
|
||||
"sort_order": 10,
|
||||
"html_content": _wrap(
|
||||
"Акт аудита организации",
|
||||
None,
|
||||
"""
|
||||
<p>Организация: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Дата: <span class="field" contenteditable="true">[дата]</span> Аудитор: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Область проверки: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Несоответствия (level 1/2): <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Корректирующие действия, срок: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="signatures"><div class="sig-block"><div class="sig-line">Аудитор</div></div><div class="sig-block"><div class="sig-line">Представитель организации</div></div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "ACT-DEFECT",
|
||||
"name": "Акт дефектации",
|
||||
"category": "act",
|
||||
"standard": "RF",
|
||||
"description": "Акт дефектации компонента/ВС",
|
||||
"sort_order": 11,
|
||||
"html_content": _wrap(
|
||||
"Акт дефектации",
|
||||
None,
|
||||
"""
|
||||
<p>ВС: <span class="field" contenteditable="true">[рег. №]</span> ATA chapter: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Описание дефекта: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>MEL категория: <span class="field" contenteditable="true">[___]</span> Решение: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="sig-block"><div class="sig-line">Подпись</div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "ACT-ACCEPT",
|
||||
"name": "Акт приёмки ВС после ТО",
|
||||
"category": "act",
|
||||
"standard": "RF",
|
||||
"description": "Акт приёмки воздушного судна после технического обслуживания",
|
||||
"sort_order": 12,
|
||||
"html_content": _wrap(
|
||||
"Акт приёмки ВС после ТО",
|
||||
None,
|
||||
"""
|
||||
<p>Наряд №: <span class="field" contenteditable="true">[___]</span> ВС: <span class="field" contenteditable="true">[рег. №]</span></p>
|
||||
<p>Выполненные работы: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Замечания: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>CRS: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<div class="signatures"><div class="sig-block"><div class="sig-line">Заказчик</div></div><div class="sig-block"><div class="sig-line">Исполнитель</div></div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "LTR-FAVT",
|
||||
"name": "Сопроводительное письмо в ФАВТ (Росавиация)",
|
||||
"category": "letter",
|
||||
"standard": "RF",
|
||||
"description": "Сопроводительное письмо в Федеральное агентство воздушного транспорта",
|
||||
"sort_order": 13,
|
||||
"html_content": _wrap(
|
||||
"Сопроводительное письмо",
|
||||
"в ФАВТ (Росавиация)",
|
||||
"""
|
||||
<p>Исх. № <span class="field" contenteditable="true">[___]</span> от <span class="field" contenteditable="true">[дата]</span></p>
|
||||
<p>Кому: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>От кого: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Тема: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Текст: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Приложения: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="sig-block"><div class="sig-line">Подпись</div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "LTR-MRO",
|
||||
"name": "Письмо-заказ в организацию ТОиР",
|
||||
"category": "letter",
|
||||
"standard": "RF",
|
||||
"description": "Письмо-заказ на выполнение работ организацией ТОиР",
|
||||
"sort_order": 14,
|
||||
"html_content": _wrap(
|
||||
"Письмо-заказ в организацию ТОиР",
|
||||
None,
|
||||
"""
|
||||
<p>Наименование ТОиР: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Запрос на выполнение работ: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>ВС: <span class="field" contenteditable="true">[рег. №]</span> Сроки: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Контакт: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="sig-block"><div class="sig-line">Подпись</div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "LTR-SB",
|
||||
"name": "Уведомление о выполнении сервисного бюллетеня (SB)",
|
||||
"category": "letter",
|
||||
"standard": "RF",
|
||||
"description": "Уведомление о выполнении SB",
|
||||
"sort_order": 15,
|
||||
"html_content": _wrap(
|
||||
"Уведомление о выполнении SB",
|
||||
None,
|
||||
"""
|
||||
<p>№ SB: <span class="field" contenteditable="true">[___]</span> ВС: <span class="field" contenteditable="true">[рег. №]</span></p>
|
||||
<p>Дата выполнения: <span class="field" contenteditable="true">[дата]</span></p>
|
||||
<p>Ссылка на наряд: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<div class="sig-block"><div class="sig-line">Подпись</div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "LTR-AD",
|
||||
"name": "Отчёт о выполнении директивы ЛГ (AD/ДЛГ)",
|
||||
"category": "letter",
|
||||
"standard": "RF",
|
||||
"description": "Отчёт о выполнении воздушной директивы",
|
||||
"sort_order": 16,
|
||||
"html_content": _wrap(
|
||||
"Отчёт о выполнении директивы ЛГ",
|
||||
"AD / ДЛГ",
|
||||
"""
|
||||
<p>№ AD: <span class="field" contenteditable="true">[___]</span> ВС: <span class="field" contenteditable="true">[рег. №]</span></p>
|
||||
<p>Метод выполнения: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Дата: <span class="field" contenteditable="true">[дата]</span></p>
|
||||
<p>Подтверждение: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="sig-block"><div class="sig-line">Подпись</div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "FORM-TECHLOG",
|
||||
"name": "Technical Log / Бортовой журнал",
|
||||
"category": "form",
|
||||
"standard": "ICAO",
|
||||
"description": "Бортовой журнал (ICAO)",
|
||||
"sort_order": 17,
|
||||
"html_content": _wrap(
|
||||
"Technical Log / Бортовой журнал",
|
||||
"ICAO",
|
||||
"""
|
||||
<table class="doc-table">
|
||||
<tr><th>Flight No</th><th>Date</th><th>Departure</th><th>Arrival</th><th>Block Time</th><th>Defects</th><th>Actions</th><th>CRS</th></tr>
|
||||
<tr><td><span class="field" contenteditable="true">[___]</span></td><td><span class="field" contenteditable="true">[___]</span></td><td><span class="field" contenteditable="true">[___]</span></td><td><span class="field" contenteditable="true">[___]</span></td><td><span class="field" contenteditable="true">[___]</span></td><td><span class="field" contenteditable="true">[___]</span></td><td><span class="field" contenteditable="true">[___]</span></td><td><span class="field" contenteditable="true">[___]</span></td></tr>
|
||||
</table>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "FORM-MEL",
|
||||
"name": "MEL Deferral Form / Форма отложенного дефекта",
|
||||
"category": "form",
|
||||
"standard": "ICAO",
|
||||
"description": "Форма отложенного дефекта по MEL",
|
||||
"sort_order": 18,
|
||||
"html_content": _wrap(
|
||||
"MEL Deferral Form",
|
||||
None,
|
||||
"""
|
||||
<p>ВС: <span class="field" contenteditable="true">[___]</span> ATA: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Item: <span class="field" contenteditable="true">[___]</span> Category (A/B/C/D): <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Deferred By: <span class="field" contenteditable="true">[___]</span> Expiry Date: <span class="field" contenteditable="true">[дата]</span></p>
|
||||
<p>Rectified By: <span class="field" contenteditable="true">[___]</span></p>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "FORM-WEIGHT",
|
||||
"name": "Weight & Balance Sheet / Весовая ведомость",
|
||||
"category": "form",
|
||||
"standard": "ICAO",
|
||||
"description": "Весовая ведомость ВС",
|
||||
"sort_order": 19,
|
||||
"html_content": _wrap(
|
||||
"Weight & Balance Sheet",
|
||||
"Весовая ведомость",
|
||||
"""
|
||||
<p>ВС: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Empty Weight: <span class="field" contenteditable="true">[___]</span> CG: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Fuel: <span class="field" contenteditable="true">[___]</span> Payload: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Max TOW: <span class="field" contenteditable="true">[___]</span> Actual TOW: <span class="field" contenteditable="true">[___]</span></p>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "FORM-WO",
|
||||
"name": "Work Order / Наряд-задание на ТО",
|
||||
"category": "form",
|
||||
"standard": "RF",
|
||||
"description": "Наряд-задание на техническое обслуживание",
|
||||
"sort_order": 20,
|
||||
"html_content": _wrap(
|
||||
"Work Order / Наряд-задание на ТО",
|
||||
None,
|
||||
"""
|
||||
<p>WO №: <span class="field" contenteditable="true">[___]</span> ВС: <span class="field" contenteditable="true">[рег. №]</span></p>
|
||||
<p>Тип работ: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Плановые ч/ч: <span class="field" contenteditable="true">[___]</span> Исполнитель: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Инструмент, запчасти: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>CRS: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<div class="sig-block"><div class="sig-line">Подпись</div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "RPT-ANNUAL",
|
||||
"name": "Годовой отчёт о состоянии парка ВС",
|
||||
"category": "report",
|
||||
"standard": "RF",
|
||||
"description": "Годовой отчёт по парку воздушных судов",
|
||||
"sort_order": 21,
|
||||
"html_content": _wrap(
|
||||
"Годовой отчёт о состоянии парка ВС",
|
||||
None,
|
||||
"""
|
||||
<div class="section-title">Парк ВС</div><p><span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="section-title">Наработка, ТО</div><p><span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="section-title">Инциденты, AD/SB, риски</div><p><span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="sig-block"><div class="sig-line">Руководитель</div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "RPT-SMS",
|
||||
"name": "Отчёт по SMS (Safety Management System)",
|
||||
"category": "report",
|
||||
"standard": "ICAO",
|
||||
"description": "Отчёт по системе управления безопасностью",
|
||||
"sort_order": 22,
|
||||
"html_content": _wrap(
|
||||
"Отчёт по SMS",
|
||||
"Safety Management System",
|
||||
"""
|
||||
<div class="section-title">Показатели безопасности (SPI)</div><p><span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="section-title">Происшествия, риски</div><p><span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="section-title">Корректирующие действия</div><p><span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="sig-block"><div class="sig-line">Подпись</div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "RPT-RISK",
|
||||
"name": "Отчёт об оценке риска",
|
||||
"category": "report",
|
||||
"standard": "RF",
|
||||
"description": "Отчёт об оценке риска",
|
||||
"sort_order": 23,
|
||||
"html_content": _wrap(
|
||||
"Отчёт об оценке риска",
|
||||
None,
|
||||
"""
|
||||
<p>Опасность: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Вероятность: <span class="field" contenteditable="true">[___]</span> Серьёзность: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Матрица риска: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Меры: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Остаточный риск: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<div class="sig-block"><div class="sig-line">Подпись</div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "ORD-PILOT",
|
||||
"name": "Приказ о допуске экипажа к полётам",
|
||||
"category": "order",
|
||||
"standard": "RF",
|
||||
"description": "Приказ о допуске пилота/экипажа к полётам",
|
||||
"sort_order": 24,
|
||||
"html_content": _wrap(
|
||||
"Приказ о допуске экипажа к полётам",
|
||||
None,
|
||||
"""
|
||||
<p>ФИО: <span class="field" contenteditable="true">[___________]</span> Должность: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Свидетельство №: <span class="field" contenteditable="true">[___]</span></p>
|
||||
<p>Типы ВС: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Дата: <span class="field" contenteditable="true">[дата]</span></p>
|
||||
<div class="sig-block"><div class="sig-line">Руководитель</div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
{
|
||||
"code": "ORD-INSPECT",
|
||||
"name": "Распоряжение о проведении инспекции",
|
||||
"category": "order",
|
||||
"standard": "RF",
|
||||
"description": "Распоряжение о проведении инспекционной проверки",
|
||||
"sort_order": 25,
|
||||
"html_content": _wrap(
|
||||
"Распоряжение о проведении инспекции",
|
||||
None,
|
||||
"""
|
||||
<p>Основание: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Объект: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Дата: <span class="field" contenteditable="true">[дата]</span> Инспектор: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<p>Задачи: <span class="field" contenteditable="true">[___________]</span></p>
|
||||
<div class="sig-block"><div class="sig-line">Подпись</div></div>
|
||||
""",
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def seed_document_templates():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
for d in _templates_data():
|
||||
if db.query(DocumentTemplate).filter(DocumentTemplate.code == d["code"]).first():
|
||||
continue
|
||||
db.add(
|
||||
DocumentTemplate(
|
||||
code=d["code"],
|
||||
name=d["name"],
|
||||
category=d["category"],
|
||||
standard=d["standard"],
|
||||
description=d.get("description"),
|
||||
html_content=d["html_content"],
|
||||
version=1,
|
||||
is_active=True,
|
||||
sort_order=d["sort_order"],
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
logger.info("Document templates seed complete: up to 25 templates")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.exception("Document templates seed failed: %s", e)
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
@ -33,6 +33,7 @@ from app.api.routes import (
|
||||
tasks_router,
|
||||
audit_router,
|
||||
ai_router,
|
||||
document_templates_router,
|
||||
)
|
||||
|
||||
|
||||
@ -47,6 +48,12 @@ async def lifespan(app: FastAPI):
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning("Checklist seed skipped: %s", e)
|
||||
try:
|
||||
from app.demo.seed_document_templates import seed_document_templates
|
||||
seed_document_templates()
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning("Document templates seed skipped: %s", e)
|
||||
# Планировщик рисков (передаём app для shutdown hook)
|
||||
setup_scheduler(app)
|
||||
yield
|
||||
@ -213,6 +220,7 @@ app.include_router(inbox_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(tasks_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(audit_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(ai_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(document_templates_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
|
||||
# WebSocket (no prefix — direct path)
|
||||
from app.api.routes.ws_notifications import router as ws_router
|
||||
|
||||
@ -17,6 +17,7 @@ from app.models.audit_log import AuditLog
|
||||
from app.models.personnel_plg import PLGSpecialist, PLGAttestation, PLGQualification
|
||||
from app.models.airworthiness_core import ADDirective, ServiceBulletin, LifeLimit, MaintenanceProgram, AircraftComponent
|
||||
from app.models.work_orders import WorkOrder
|
||||
from app.models.document_template import DocumentTemplate
|
||||
from app.models.legal import (
|
||||
DocumentType,
|
||||
Jurisdiction,
|
||||
@ -67,4 +68,5 @@ __all__ = [
|
||||
"MaintenanceProgram",
|
||||
"AircraftComponent",
|
||||
"WorkOrder",
|
||||
"DocumentTemplate",
|
||||
]
|
||||
|
||||
27
backend/app/models/document_template.py
Normal file
27
backend/app/models/document_template.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""Шаблоны документов авиационной отрасли."""
|
||||
from sqlalchemy import String, Text, Integer, Boolean
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
from app.models.common import TimestampMixin, uuid4_str
|
||||
|
||||
|
||||
class DocumentTemplate(Base, TimestampMixin):
|
||||
__tablename__ = "document_templates"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid4_str)
|
||||
code: Mapped[str] = mapped_column(String(32), unique=True, nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
category: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False,
|
||||
doc="checklist|letter|application|certificate|report|act|order|form"
|
||||
)
|
||||
standard: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False,
|
||||
doc="RF|ICAO|EASA|FAA|INTERNAL"
|
||||
)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
html_content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
version: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
95
components/DocumentPreviewModal.tsx
Normal file
95
components/DocumentPreviewModal.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
import { documentTemplatesApi } from '@/lib/api/api-client';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
|
||||
interface Props {
|
||||
template: { id: string; name: string; code: string; html_content: string };
|
||||
onClose: () => void;
|
||||
onSaved?: () => void;
|
||||
}
|
||||
|
||||
export default function DocumentPreviewModal({ template, onClose, onSaved }: Props) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const { hasRole } = useAuth();
|
||||
const canEdit = hasRole('admin') || hasRole('authority_inspector');
|
||||
|
||||
const handlePrint = () => {
|
||||
window.print();
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!contentRef.current || !canEdit) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const html = contentRef.current.innerHTML;
|
||||
await documentTemplatesApi.update(template.id, { html_content: html });
|
||||
onSaved?.();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
@media print {
|
||||
body * { visibility: hidden; }
|
||||
.document-preview-modal-print, .document-preview-modal-print * { visibility: visible; }
|
||||
.document-preview-modal-print { position: absolute; left: 0; top: 0; width: 100%; background: white; }
|
||||
.document-preview-modal-print .document-print-header { display: none; }
|
||||
}
|
||||
`}</style>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 document-preview-modal-print" onClick={onClose}>
|
||||
<div
|
||||
className="flex max-h-[95vh] w-full max-w-5xl flex-col rounded-lg bg-white shadow-xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="document-print-header flex items-center justify-between border-b border-gray-200 px-4 py-3">
|
||||
<div>
|
||||
<span className="font-bold text-gray-800">{template.name}</span>
|
||||
<span className="ml-2 text-sm text-gray-500">{template.code}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePrint}
|
||||
className="rounded bg-primary-500 px-3 py-1.5 text-sm text-white hover:bg-primary-600"
|
||||
>
|
||||
🖨️ Печать
|
||||
</button>
|
||||
{canEdit && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="rounded bg-green-600 px-3 py-1.5 text-sm text-white hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Сохранение…' : '💾 Сохранить изменения'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded bg-gray-200 px-3 py-1.5 text-sm hover:bg-gray-300"
|
||||
>
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4 print:overflow-visible">
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="document-content mx-auto max-w-[210mm]"
|
||||
dangerouslySetInnerHTML={{ __html: template.html_content }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -30,6 +30,7 @@ const menuItems: MenuItem[] = [
|
||||
{ name: 'Дефекты', path: '/defects', icon: '🛠️' },
|
||||
{ name: 'Модификации', path: '/modifications', icon: '⚙️' },
|
||||
{ name: 'Документы', path: '/documents', icon: '📄' },
|
||||
{ name: 'Шаблоны', path: '/templates', icon: '📋' },
|
||||
{ name: 'Inbox', path: '/inbox', icon: '📥' },
|
||||
{ name: 'Нормативные документы', path: '/regulations', icon: '📚' },
|
||||
{ name: 'Мониторинг', path: '/monitoring', icon: '📈', roles: ['admin', 'authority_inspector'] },
|
||||
|
||||
@ -243,6 +243,15 @@ export const attachmentsApi = {
|
||||
downloadUrl: (id: string) => `${API_BASE}/attachments/${id}/download`,
|
||||
};
|
||||
|
||||
// Document Templates (библиотека шаблонов REFLY)
|
||||
export const documentTemplatesApi = {
|
||||
list: (params?: QueryParams) =>
|
||||
apiFetch<PaginatedResponse<any>>(`/document-templates${buildQuery(params)}`),
|
||||
get: (id: string) => apiFetch(`/document-templates/${id}`),
|
||||
update: (id: string, data: any) =>
|
||||
apiFetch(`/document-templates/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
||||
};
|
||||
|
||||
// Health
|
||||
export const healthApi = {
|
||||
check: () => apiFetch('/health'),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user