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: 'Сертификаты ЛГ', href: '/airworthiness', icon: '📜' },
|
||||||
{ title: 'Нормативные документы', desc: 'ФАП, ICAO, EASA', href: '/regulations', icon: '📚' },
|
{ title: 'Нормативные документы', desc: 'ФАП, ICAO, EASA', href: '/regulations', icon: '📚' },
|
||||||
{ title: 'Чек-листы', desc: 'Шаблоны проверок', href: '/checklists', icon: '✅' },
|
{ title: 'Чек-листы', desc: 'Шаблоны проверок', href: '/checklists', icon: '✅' },
|
||||||
|
{ title: 'Шаблоны документов', desc: 'Заявки, акты, письма, формы', href: '/templates', icon: '📋' },
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<PageLayout title="Документы" subtitle="Просмотр документов, прикреплённых к ВС, аудитам и заявкам">
|
<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 .tasks import router as tasks_router
|
||||||
from .audit import router as audit_router
|
from .audit import router as audit_router
|
||||||
from .ai import router as ai_router
|
from .ai import router as ai_router
|
||||||
|
from .document_templates import router as document_templates_router
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"health_router",
|
"health_router",
|
||||||
@ -38,5 +39,6 @@ __all__ = [
|
|||||||
"tasks_router",
|
"tasks_router",
|
||||||
"audit_router",
|
"audit_router",
|
||||||
"ai_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
|
from app.demo.seed_checklists import seed_checklists
|
||||||
seed_checklists()
|
seed_checklists()
|
||||||
|
from app.demo.seed_document_templates import seed_document_templates
|
||||||
|
seed_document_templates()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
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,
|
tasks_router,
|
||||||
audit_router,
|
audit_router,
|
||||||
ai_router,
|
ai_router,
|
||||||
|
document_templates_router,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -47,6 +48,12 @@ async def lifespan(app: FastAPI):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
import logging
|
import logging
|
||||||
logging.getLogger(__name__).warning("Checklist seed skipped: %s", e)
|
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)
|
# Планировщик рисков (передаём app для shutdown hook)
|
||||||
setup_scheduler(app)
|
setup_scheduler(app)
|
||||||
yield
|
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(tasks_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||||
app.include_router(audit_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(ai_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||||
|
app.include_router(document_templates_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||||
|
|
||||||
# WebSocket (no prefix — direct path)
|
# WebSocket (no prefix — direct path)
|
||||||
from app.api.routes.ws_notifications import router as ws_router
|
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.personnel_plg import PLGSpecialist, PLGAttestation, PLGQualification
|
||||||
from app.models.airworthiness_core import ADDirective, ServiceBulletin, LifeLimit, MaintenanceProgram, AircraftComponent
|
from app.models.airworthiness_core import ADDirective, ServiceBulletin, LifeLimit, MaintenanceProgram, AircraftComponent
|
||||||
from app.models.work_orders import WorkOrder
|
from app.models.work_orders import WorkOrder
|
||||||
|
from app.models.document_template import DocumentTemplate
|
||||||
from app.models.legal import (
|
from app.models.legal import (
|
||||||
DocumentType,
|
DocumentType,
|
||||||
Jurisdiction,
|
Jurisdiction,
|
||||||
@ -67,4 +68,5 @@ __all__ = [
|
|||||||
"MaintenanceProgram",
|
"MaintenanceProgram",
|
||||||
"AircraftComponent",
|
"AircraftComponent",
|
||||||
"WorkOrder",
|
"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: '/defects', icon: '🛠️' },
|
||||||
{ name: 'Модификации', path: '/modifications', icon: '⚙️' },
|
{ name: 'Модификации', path: '/modifications', icon: '⚙️' },
|
||||||
{ name: 'Документы', path: '/documents', icon: '📄' },
|
{ name: 'Документы', path: '/documents', icon: '📄' },
|
||||||
|
{ name: 'Шаблоны', path: '/templates', icon: '📋' },
|
||||||
{ name: 'Inbox', path: '/inbox', icon: '📥' },
|
{ name: 'Inbox', path: '/inbox', icon: '📥' },
|
||||||
{ name: 'Нормативные документы', path: '/regulations', icon: '📚' },
|
{ name: 'Нормативные документы', path: '/regulations', icon: '📚' },
|
||||||
{ name: 'Мониторинг', path: '/monitoring', icon: '📈', roles: ['admin', 'authority_inspector'] },
|
{ name: 'Мониторинг', path: '/monitoring', icon: '📈', roles: ['admin', 'authority_inspector'] },
|
||||||
|
|||||||
@ -243,6 +243,15 @@ export const attachmentsApi = {
|
|||||||
downloadUrl: (id: string) => `${API_BASE}/attachments/${id}/download`,
|
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
|
// Health
|
||||||
export const healthApi = {
|
export const healthApi = {
|
||||||
check: () => apiFetch('/health'),
|
check: () => apiFetch('/health'),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user