feat: 8 шаблонов чек-листов аудита ВС (РФ/ИКАО/EASA/FAA) + редактирование
- seed_checklists.py: 8 шаблонов (~120 пунктов) — ФАП-148, ФАП-145, ФАП-147, ICAO Annex 8, ICAO Annex 19 SMS, EASA Part-M, EASA Part-CAMO, FAA Part 43/91 - checklists.py: PATCH template, PATCH/POST/DELETE items - ChecklistEditModal.tsx: модальное окно с inline-edit пунктов - api-client.ts: updateTemplate, addItem, updateItem, deleteItem - FilterBar: фильтры по стандартам (ФАП/ИКАО/EASA/FAA) - Автозагрузка шаблонов при первом запуске (lifespan + demo seed) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
646401299c
commit
62958239ac
@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import ChecklistCardModal from '@/components/ChecklistCardModal';
|
||||
import ChecklistEditModal from '@/components/ChecklistEditModal';
|
||||
import { useState } from 'react';
|
||||
import ChecklistCreateModal from '@/components/ChecklistCreateModal';
|
||||
import { PageLayout, FilterBar, EmptyState } from '@/components/ui';
|
||||
@ -12,6 +13,7 @@ export default function ChecklistsPage() {
|
||||
const { data, isLoading, mutate } = useChecklistsData({ domain });
|
||||
const templates = data?.items || [];
|
||||
const [exp, setExp] = useState<string | null>(null);
|
||||
const [editTemplate, setEditTemplate] = useState<any | null>(null);
|
||||
|
||||
const gen = async (src: string) => { const n = prompt('Название:'); if (!n) return; await checklistsApi.generate(src, n); mutate(); };
|
||||
|
||||
@ -22,14 +24,28 @@ export default function ChecklistsPage() {
|
||||
<button onClick={() => gen('custom')} className="btn-primary bg-blue-500 hover:bg-blue-600">+ Пользовательский</button>
|
||||
</RequireRole>}>
|
||||
<FilterBar value={domain} onChange={setDomain} className="mb-4"
|
||||
options={[{ value: undefined, label: 'Все' }, { value: 'ФАП-М', label: 'ФАП-М' }, { value: 'ATA', label: 'ATA' }, { value: 'CSV', label: 'CSV' }]} />
|
||||
options={[
|
||||
{ value: undefined, label: 'Все' },
|
||||
{ value: 'ФАП-148', label: '🇷🇺 ФАП-148' },
|
||||
{ value: 'ФАП-145', label: '🇷🇺 ФАП-145' },
|
||||
{ value: 'ФАП-147', label: '🇷🇺 ФАП-147' },
|
||||
{ value: 'ICAO', label: '🌐 ИКАО' },
|
||||
{ value: 'EASA', label: '🇪🇺 EASA' },
|
||||
{ value: 'FAA', label: '🇺🇸 FAA' },
|
||||
{ value: 'CSV', label: '📄 CSV' },
|
||||
]} />
|
||||
{!isLoading && templates.length > 0 ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
{templates.map((t: any) => (
|
||||
<div key={t.id} className="card">
|
||||
<div className="p-5 flex justify-between items-center cursor-pointer" onClick={() => setExp(exp === t.id ? null : t.id)}>
|
||||
<div><div className="font-bold">{t.name}</div><div className="text-xs text-gray-500">{t.domain || '—'} · v{t.version || 1} · {t.items?.length || 0} пунктов</div></div>
|
||||
<span className="text-lg">{exp === t.id ? '▼' : '▶'}</span>
|
||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<RequireRole roles={['admin', 'authority_inspector']}>
|
||||
<button type="button" onClick={() => setEditTemplate(t)} className="rounded px-2 py-1 text-sm hover:bg-gray-100" title="Редактировать">✏️ Редактировать</button>
|
||||
</RequireRole>
|
||||
<span className="text-lg">{exp === t.id ? '▼' : '▶'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{exp === t.id && t.items?.length > 0 && (
|
||||
<div className="border-t border-gray-100 px-5 pb-4">
|
||||
@ -45,6 +61,13 @@ export default function ChecklistsPage() {
|
||||
))}
|
||||
</div>
|
||||
) : !isLoading ? <EmptyState message="Нет шаблонов. Создайте через кнопку выше." /> : null}
|
||||
{editTemplate && (
|
||||
<ChecklistEditModal
|
||||
template={editTemplate}
|
||||
onClose={() => setEditTemplate(null)}
|
||||
onSaved={() => mutate()}
|
||||
/>
|
||||
)}
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,7 +7,14 @@ from app.api.deps import get_current_user, require_roles
|
||||
from app.api.helpers import audit, paginate_query
|
||||
from app.api.deps import get_db
|
||||
from app.models import ChecklistTemplate, ChecklistItem
|
||||
from app.schemas.audit import ChecklistTemplateCreate, ChecklistTemplateOut, ChecklistItemCreate, ChecklistItemOut
|
||||
from app.schemas.audit import (
|
||||
ChecklistTemplateCreate,
|
||||
ChecklistTemplateOut,
|
||||
ChecklistTemplateUpdate,
|
||||
ChecklistItemCreate,
|
||||
ChecklistItemOut,
|
||||
ChecklistItemUpdate,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["checklists"])
|
||||
|
||||
@ -75,6 +82,93 @@ def get_template(template_id: str, db: Session = Depends(get_db), user=Depends(g
|
||||
return _template_with_items(t, db)
|
||||
|
||||
|
||||
@router.patch("/checklists/templates/{template_id}", response_model=ChecklistTemplateOut,
|
||||
dependencies=[Depends(require_roles("admin", "authority_inspector"))])
|
||||
def update_template(
|
||||
template_id: str,
|
||||
payload: ChecklistTemplateUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
t = db.query(ChecklistTemplate).filter(ChecklistTemplate.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.domain is not None:
|
||||
t.domain = payload.domain
|
||||
audit(db, user, "update", "checklist_template", entity_id=template_id)
|
||||
db.commit()
|
||||
db.refresh(t)
|
||||
return _template_with_items(t, db)
|
||||
|
||||
|
||||
@router.patch("/checklists/items/{item_id}", response_model=ChecklistItemOut,
|
||||
dependencies=[Depends(require_roles("admin", "authority_inspector"))])
|
||||
def update_item(
|
||||
item_id: str,
|
||||
payload: ChecklistItemUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
item = db.query(ChecklistItem).filter(ChecklistItem.id == item_id).first()
|
||||
if not item:
|
||||
raise HTTPException(404, "Item not found")
|
||||
if payload.code is not None:
|
||||
item.code = payload.code
|
||||
if payload.text is not None:
|
||||
item.text = payload.text
|
||||
if payload.sort_order is not None:
|
||||
item.sort_order = payload.sort_order
|
||||
audit(db, user, "update", "checklist_item", entity_id=item_id)
|
||||
db.commit()
|
||||
db.refresh(item)
|
||||
return ChecklistItemOut.model_validate(item)
|
||||
|
||||
|
||||
@router.post("/checklists/templates/{template_id}/items", response_model=ChecklistItemOut, status_code=201,
|
||||
dependencies=[Depends(require_roles("admin", "authority_inspector"))])
|
||||
def add_item(
|
||||
template_id: str,
|
||||
payload: ChecklistItemCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
t = db.query(ChecklistTemplate).filter(ChecklistTemplate.id == template_id).first()
|
||||
if not t:
|
||||
raise HTTPException(404, "Template not found")
|
||||
max_order = db.query(ChecklistItem).filter(ChecklistItem.template_id == template_id).count()
|
||||
item = ChecklistItem(
|
||||
template_id=template_id,
|
||||
code=payload.code,
|
||||
text=payload.text,
|
||||
domain=payload.domain,
|
||||
sort_order=payload.sort_order if payload.sort_order else (max_order + 1),
|
||||
)
|
||||
db.add(item)
|
||||
audit(db, user, "create", "checklist_item")
|
||||
db.commit()
|
||||
db.refresh(item)
|
||||
return ChecklistItemOut.model_validate(item)
|
||||
|
||||
|
||||
@router.delete("/checklists/items/{item_id}", status_code=204,
|
||||
dependencies=[Depends(require_roles("admin", "authority_inspector"))])
|
||||
def delete_item(
|
||||
item_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
item = db.query(ChecklistItem).filter(ChecklistItem.id == item_id).first()
|
||||
if not item:
|
||||
raise HTTPException(404, "Item not found")
|
||||
audit(db, user, "delete", "checklist_item", entity_id=item_id)
|
||||
db.delete(item)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.post("/checklists/generate-from-csv", response_model=ChecklistTemplateOut,
|
||||
dependencies=[Depends(require_roles("admin", "authority_inspector"))])
|
||||
async def generate_from_csv(file: UploadFile = File(...), name: str | None = None, domain: str | None = None,
|
||||
|
||||
1
backend/app/demo/__init__.py
Normal file
1
backend/app/demo/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Demo: seed и утилиты для demo-развёртывания
|
||||
237
backend/app/demo/seed.py
Normal file
237
backend/app/demo/seed.py
Normal file
@ -0,0 +1,237 @@
|
||||
"""
|
||||
Наполнение БД demo-данными: организации, типы ВС, воздушные суда, заявки, пользователи.
|
||||
Запуск: python -m app.demo.seed
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from app.db.session import SessionLocal, engine
|
||||
from app.db.base import Base
|
||||
from app.models import (
|
||||
Organization,
|
||||
Aircraft,
|
||||
AircraftType,
|
||||
CertApplication,
|
||||
CertApplicationStatus,
|
||||
Notification,
|
||||
User,
|
||||
)
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def seed():
|
||||
logger.info("Creating tables...")
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Проверка: если данные уже есть — пропускаем
|
||||
if db.query(Organization).first():
|
||||
logger.info("Demo data already exists, skipping.")
|
||||
return
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# ── 5 тестовых пользователей (для JWT и created_by) ──
|
||||
demo_users = [
|
||||
User(
|
||||
id="demo-admin-001",
|
||||
external_subject="demo-admin-001",
|
||||
display_name="Админ Системы",
|
||||
email="admin@demo.klg.refly.ru",
|
||||
role="admin",
|
||||
organization_id=None,
|
||||
),
|
||||
User(
|
||||
id="demo-inspector-001",
|
||||
external_subject="demo-inspector-001",
|
||||
display_name="Иванов И.П. (Инспектор ФАВТ)",
|
||||
email="inspector@demo.klg.refly.ru",
|
||||
role="authority_inspector",
|
||||
organization_id=None,
|
||||
),
|
||||
User(
|
||||
id="demo-operator-mgr-001",
|
||||
external_subject="demo-operator-mgr-001",
|
||||
display_name="Петров А.С. (Оператор — руководитель)",
|
||||
email="operator-mgr@demo.klg.refly.ru",
|
||||
role="operator_manager",
|
||||
organization_id="org-airline-001",
|
||||
),
|
||||
User(
|
||||
id="demo-mro-mgr-001",
|
||||
external_subject="demo-mro-mgr-001",
|
||||
display_name="Сидоров В.Н. (ТОиР — руководитель)",
|
||||
email="mro-mgr@demo.klg.refly.ru",
|
||||
role="mro_manager",
|
||||
organization_id="org-mro-001",
|
||||
),
|
||||
User(
|
||||
id="demo-operator-user-001",
|
||||
external_subject="demo-operator-user-001",
|
||||
display_name="Козлова Е.А. (Оператор — специалист)",
|
||||
email="operator@demo.klg.refly.ru",
|
||||
role="operator_user",
|
||||
organization_id="org-airline-001",
|
||||
),
|
||||
]
|
||||
db.add_all(demo_users)
|
||||
logger.info("Added 5 demo users")
|
||||
|
||||
# ── Организации (kind, не type) ───────────────────────
|
||||
orgs = [
|
||||
Organization(
|
||||
id="org-airline-001",
|
||||
name="АК «КалинингрАвиа»",
|
||||
kind="operator",
|
||||
inn="3901234567",
|
||||
ogrn="1023901234567",
|
||||
address="г. Калининград, ул. Авиационная, д. 1",
|
||||
),
|
||||
Organization(
|
||||
id="org-mro-001",
|
||||
name="ТОиР «Балтик Техник»",
|
||||
kind="mro",
|
||||
inn="3907654321",
|
||||
ogrn="1023907654321",
|
||||
address="г. Калининград, аэропорт Храброво, ангар 3",
|
||||
),
|
||||
Organization(
|
||||
id="org-authority-001",
|
||||
name="МТУ ФАВТ Северо-Запад",
|
||||
kind="authority",
|
||||
inn="7801234567",
|
||||
ogrn="1027801234567",
|
||||
address="г. Санкт-Петербург, Московский пр., д. 10",
|
||||
),
|
||||
]
|
||||
db.add_all(orgs)
|
||||
logger.info("Added 3 organizations")
|
||||
|
||||
# ── Типы ВС (model, manufacturer, icao_code) ───────────
|
||||
types = [
|
||||
AircraftType(
|
||||
id="type-ssj100",
|
||||
model="Sukhoi Superjet 100",
|
||||
manufacturer="ПАО «Яковлев»",
|
||||
icao_code="SU95",
|
||||
),
|
||||
AircraftType(
|
||||
id="type-mc21",
|
||||
model="МС-21-300",
|
||||
manufacturer="ПАО «Яковлев»",
|
||||
icao_code="M321",
|
||||
),
|
||||
AircraftType(
|
||||
id="type-l410",
|
||||
model="L-410 UVP-E20",
|
||||
manufacturer="Aircraft Industries",
|
||||
icao_code="L410",
|
||||
),
|
||||
]
|
||||
db.add_all(types)
|
||||
logger.info("Added 3 aircraft types")
|
||||
|
||||
# ── Воздушные суда (без flight_hours — нет в модели aircraft_db) ──
|
||||
aircraft = [
|
||||
Aircraft(
|
||||
id="ac-001",
|
||||
registration_number="RA-89001",
|
||||
serial_number="95001",
|
||||
aircraft_type_id="type-ssj100",
|
||||
operator_id="org-airline-001",
|
||||
status="active",
|
||||
year_of_manufacture=2020,
|
||||
),
|
||||
Aircraft(
|
||||
id="ac-002",
|
||||
registration_number="RA-89002",
|
||||
serial_number="95002",
|
||||
aircraft_type_id="type-ssj100",
|
||||
operator_id="org-airline-001",
|
||||
status="active",
|
||||
year_of_manufacture=2021,
|
||||
),
|
||||
Aircraft(
|
||||
id="ac-003",
|
||||
registration_number="RA-73001",
|
||||
serial_number="21001",
|
||||
aircraft_type_id="type-mc21",
|
||||
operator_id="org-airline-001",
|
||||
status="maintenance",
|
||||
year_of_manufacture=2024,
|
||||
),
|
||||
Aircraft(
|
||||
id="ac-004",
|
||||
registration_number="RA-67001",
|
||||
serial_number="3001",
|
||||
aircraft_type_id="type-l410",
|
||||
operator_id="org-airline-001",
|
||||
status="active",
|
||||
year_of_manufacture=2019,
|
||||
),
|
||||
]
|
||||
db.add_all(aircraft)
|
||||
logger.info("Added 4 aircraft")
|
||||
|
||||
# ── Заявки на сертификацию ─────────────────────────────
|
||||
today_str = now.strftime("%Y%m%d")
|
||||
apps = [
|
||||
CertApplication(
|
||||
id="app-001",
|
||||
number=f"KLG-{today_str}-0001",
|
||||
applicant_org_id="org-mro-001",
|
||||
created_by_user_id="demo-mro-mgr-001",
|
||||
status=CertApplicationStatus.SUBMITTED,
|
||||
subject="Сертификация ТОиР «Балтик Техник» по ФАП-145",
|
||||
description="Первичная сертификация организации по техническому обслуживанию. "
|
||||
"Scope: SSJ100, L-410. Категории: A (base), C (engine).",
|
||||
submitted_at=now - timedelta(days=5),
|
||||
),
|
||||
CertApplication(
|
||||
id="app-002",
|
||||
number=f"KLG-{today_str}-0002",
|
||||
applicant_org_id="org-airline-001",
|
||||
created_by_user_id="demo-operator-mgr-001",
|
||||
status=CertApplicationStatus.DRAFT,
|
||||
subject="Продление сертификата эксплуатанта АК «КалинингрАвиа»",
|
||||
description="Продление СЭ. 4 ВС в парке: 2x SSJ100, 1x МС-21, 1x L-410.",
|
||||
),
|
||||
]
|
||||
db.add_all(apps)
|
||||
logger.info("Added 2 cert applications")
|
||||
|
||||
# ── Уведомления ───────────────────────────────────────
|
||||
notifs = [
|
||||
Notification(
|
||||
recipient_user_id="demo-inspector-001",
|
||||
title="Новая заявка: ТОиР «Балтик Техник»",
|
||||
body="Заявка KLG-...-0001 подана на рассмотрение.",
|
||||
),
|
||||
Notification(
|
||||
recipient_user_id="demo-operator-mgr-001",
|
||||
title="Приближается срок инспекции RA-89001",
|
||||
body="Плановая инспекция через 14 дней.",
|
||||
),
|
||||
]
|
||||
db.add_all(notifs)
|
||||
|
||||
db.commit()
|
||||
logger.info("✅ Demo seed complete: 5 users, 3 orgs, 3 types, 4 aircraft, 2 applications")
|
||||
|
||||
from app.demo.seed_checklists import seed_checklists
|
||||
seed_checklists()
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error("Seed failed: %s", e)
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed()
|
||||
178
backend/app/demo/seed_checklists.py
Normal file
178
backend/app/demo/seed_checklists.py
Normal file
@ -0,0 +1,178 @@
|
||||
"""
|
||||
Seed 8 шаблонов чек-листов аудита ВС (РФ/ИКАО/EASA/FAA).
|
||||
Проверка на дубликаты по имени перед созданием.
|
||||
"""
|
||||
import logging
|
||||
from app.db.session import SessionLocal
|
||||
from app.models import ChecklistTemplate, ChecklistItem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _ensure_template(db, name: str, description: str | None, domain: str, items: list[tuple[str, str]]) -> None:
|
||||
if db.query(ChecklistTemplate).filter(ChecklistTemplate.name == name).first():
|
||||
return
|
||||
t = ChecklistTemplate(name=name, version=1, description=description, domain=domain, is_active=True)
|
||||
db.add(t)
|
||||
db.flush()
|
||||
for sort_order, (code, text) in enumerate(items, start=1):
|
||||
db.add(ChecklistItem(template_id=t.id, code=code, text=text, domain=domain, sort_order=sort_order))
|
||||
logger.info("Created template: %s (%s items)", name, len(items))
|
||||
|
||||
|
||||
def seed_checklists():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# 1. ФАП-148 — Поддержание лётной годности ВС (15 пунктов)
|
||||
_ensure_template(db, "ФАП-148 — Поддержание лётной годности ВС", None, "ФАП-148", [
|
||||
("148.1", "Наличие действующего сертификата лётной годности (СЛГ)"),
|
||||
("148.2", "СЛГ соответствует типу и модификации ВС"),
|
||||
("148.3", "Наличие утверждённой программы ТО (ПрТО)"),
|
||||
("148.4", "ПрТО соответствует данным разработчика и требованиям ГосЦЕНТР"),
|
||||
("148.5", "Все директивы лётной годности (ДЛГ) выполнены в установленные сроки"),
|
||||
("148.6", "Информация о выполнении ДЛГ внесена в формуляр ВС"),
|
||||
("148.7", "Бюллетени (SB) категории «обязательные» выполнены"),
|
||||
("148.8", "Наработка ВС (часы, циклы) соответствует записям в формуляре"),
|
||||
("148.9", "Агрегаты с ограниченным ресурсом не превышают назначенных пределов"),
|
||||
("148.10", "Весовая ведомость и центровка актуальны"),
|
||||
("148.11", "Записи о последнем ТО формы A-check / C-check имеются"),
|
||||
("148.12", "Все открытые дефекты оформлены по MEL/CDL или устранены"),
|
||||
("148.13", "Бортовая документация укомплектована и актуальна"),
|
||||
("148.14", "Свидетельство о регистрации действительно"),
|
||||
("148.15", "Страховой полис ОСАГО действителен"),
|
||||
])
|
||||
|
||||
# 2. ФАП-145 — Аудит организации ТОиР (18 пунктов)
|
||||
_ensure_template(db, "ФАП-145 — Аудит организации ТОиР", None, "ФАП-145", [
|
||||
("145.A.25", "Наличие одобрения / сертификата ТОиР"),
|
||||
("145.A.30a", "Требования к персоналу: ответственный руководитель назначен"),
|
||||
("145.A.30b", "Система качества: менеджер по качеству назначен, независим"),
|
||||
("145.A.35", "Сертифицирующий персонал: квалификация подтверждена"),
|
||||
("145.A.40a", "Оборудование и инструмент: перечень актуален"),
|
||||
("145.A.40b", "Калибровка инструмента выполнена в срок"),
|
||||
("145.A.42", "Компоненты: входной контроль, прослеживаемость, маркировка"),
|
||||
("145.A.45a", "Техническая документация: актуальность и доступность"),
|
||||
("145.A.45b", "Данные производителя (AMM, IPC, SRM) в актуальной ревизии"),
|
||||
("145.A.47", "Планирование ТО: ресурсы, сроки, приоритеты"),
|
||||
("145.A.50a", "Выполнение ТО: соответствие наряду и карте ТО"),
|
||||
("145.A.50b", "CRS (сертификат допуска к эксплуатации) оформлен правильно"),
|
||||
("145.A.55", "Записи о ТО: хранение не менее 3 лет"),
|
||||
("145.A.60", "Происшествия/инциденты: сообщения поданы"),
|
||||
("145.A.65a", "Политика безопасности: SMS внедрена"),
|
||||
("145.A.65b", "Система управления рисками функционирует"),
|
||||
("145.A.70", "Руководство MRO (MOE) актуально и одобрено"),
|
||||
("145.A.75", "Привилегии: работы выполняются в рамках области одобрения"),
|
||||
])
|
||||
|
||||
# 3. ФАП-147 — Аудит учебной организации (9 пунктов)
|
||||
_ensure_template(db, "ФАП-147 — Аудит учебной организации", None, "ФАП-147", [
|
||||
("147.A.05", "Область одобрения"),
|
||||
("147.A.10", "Персонал"),
|
||||
("147.A.15", "Помещения"),
|
||||
("147.A.20", "Оборудование"),
|
||||
("147.A.25", "Программы"),
|
||||
("147.A.30", "Экзамены"),
|
||||
("147.A.35", "Записи"),
|
||||
("147.A.40", "MTOE"),
|
||||
("147.A.45", "Привилегии"),
|
||||
])
|
||||
|
||||
# 4. ICAO Annex 8 — Лётная годность ВС (12 пунктов)
|
||||
_ensure_template(db, "ICAO Annex 8 — Лётная годность ВС", None, "ICAO", [
|
||||
("ICAO-8.3.1", "Сертификаты типа"),
|
||||
("ICAO-8.3.2", "Сертификаты лётной годности"),
|
||||
("ICAO-8.3.3", "Надзор за ЛГ"),
|
||||
("ICAO-8.3.4", "Надзор (продолжение)"),
|
||||
("ICAO-8.4.1", "Программа ТО"),
|
||||
("ICAO-8.4.2", "Директивы (AD)"),
|
||||
("ICAO-8.4.3", "Модификации"),
|
||||
("ICAO-8.4.4", "Ремонты"),
|
||||
("ICAO-8.5.1", "Документация"),
|
||||
("ICAO-8.5.2", "Весовая ведомость"),
|
||||
("ICAO-8.6.1", "Годность после ТО"),
|
||||
("ICAO-8.6.2", "CRS"),
|
||||
])
|
||||
|
||||
# 5. ICAO Annex 19 — SMS (12 пунктов)
|
||||
_ensure_template(db, "ICAO Annex 19 — SMS", None, "ICAO", [
|
||||
("SMS-1.1", "Политика безопасности — утверждена"),
|
||||
("SMS-1.2", "Политика — доведена до персонала"),
|
||||
("SMS-1.3", "Политика — пересмотр"),
|
||||
("SMS-2.1", "Управление рисками — идентификация"),
|
||||
("SMS-2.2", "Управление рисками — оценка"),
|
||||
("SMS-2.3", "Управление рисками — митигация"),
|
||||
("SMS-3.1", "Обеспечение безопасности — обеспечение"),
|
||||
("SMS-3.2", "Обеспечение безопасности — мониторинг"),
|
||||
("SMS-3.3", "Обеспечение безопасности — улучшение"),
|
||||
("SMS-4.1", "Содействие — обучение"),
|
||||
("SMS-4.2", "Содействие — коммуникация"),
|
||||
("SMS-4.3", "Содействие — отчетность"),
|
||||
])
|
||||
|
||||
# 6. EASA Part-M — Поддержание ЛГ (20 пунктов)
|
||||
_ensure_template(db, "EASA Part-M — Поддержание ЛГ", None, "EASA", [
|
||||
("M.A.201", "Ответственность эксплуатанта"),
|
||||
("M.A.301a", "ТО — соответствие"),
|
||||
("M.A.301b", "ТО — AD выполнены"),
|
||||
("M.A.301c", "Дефекты — устранены или разрешены"),
|
||||
("M.A.302", "Программа ТО"),
|
||||
("M.A.303", "Директивы"),
|
||||
("M.A.304", "Модификации"),
|
||||
("M.A.305", "Документация"),
|
||||
("M.A.306", "Записи ТО"),
|
||||
("M.A.307", "Хранение записей"),
|
||||
("M.A.401", "Выполнение ТО"),
|
||||
("M.A.402", "Дефекты при ТО"),
|
||||
("M.A.403", "Компоненты — пригодность"),
|
||||
("M.A.501", "Компоненты — установка"),
|
||||
("M.A.502", "Компоненты — прослеживаемость"),
|
||||
("M.A.801", "CRS"),
|
||||
("M.A.901", "ARC — выдача"),
|
||||
("M.A.902", "ARC — хранение"),
|
||||
])
|
||||
|
||||
# 7. EASA Part-CAMO — Аудит CAMO (12 пунктов)
|
||||
_ensure_template(db, "EASA Part-CAMO — Аудит CAMO", None, "EASA", [
|
||||
("CAMO.A.200", "Одобрение CAMO"),
|
||||
("CAMO.A.205", "Управление воздушным судном (AM)"),
|
||||
("CAMO.A.210", "Персонал"),
|
||||
("CAMO.A.215", "Помещения"),
|
||||
("CAMO.A.300", "Программа ТО"),
|
||||
("CAMO.A.305", "AD — выполнение"),
|
||||
("CAMO.A.310", "Компоненты"),
|
||||
("CAMO.A.315", "Дефекты"),
|
||||
("CAMO.A.320", "ARC"),
|
||||
("CAMO.A.325", "Записи ARC"),
|
||||
("CAMO.A.405", "Качество"),
|
||||
("CAMO.A.410", "SMS"),
|
||||
])
|
||||
|
||||
# 8. FAA Part 43/91 — Annual/100-hour Inspection (17 пунктов)
|
||||
_ensure_template(db, "FAA Part 43/91 — Annual/100-hour Inspection", None, "FAA", [
|
||||
("43.D-1", "Fuselage and cabin"),
|
||||
("43.D-2", "Cabin — emergency equipment"),
|
||||
("43.D-3", "Engine and nacelle"),
|
||||
("43.D-4", "Engine — compressor"),
|
||||
("43.D-5", "Propeller"),
|
||||
("43.D-6", "Landing gear"),
|
||||
("43.D-7", "Wing and center section"),
|
||||
("43.D-8", "Empennage"),
|
||||
("43.D-9", "Control cables"),
|
||||
("43.D-10", "Instruments"),
|
||||
("43.D-11", "Avionics"),
|
||||
("43.D-12", "Fuel system"),
|
||||
("43.D-13", "Hydraulic system"),
|
||||
("43.D-14", "Miscellaneous"),
|
||||
("43.D-15", "Engine — final check"),
|
||||
("91.409", "Inspection status"),
|
||||
("91.417", "Records"),
|
||||
])
|
||||
|
||||
db.commit()
|
||||
logger.info("Checklist seed complete: 8 templates")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.exception("Checklist seed failed: %s", e)
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
@ -40,6 +40,12 @@ async def lifespan(app: FastAPI):
|
||||
"""Startup / shutdown events."""
|
||||
# Create tables if they don't exist (dev only; production uses Alembic)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
try:
|
||||
from app.demo.seed_checklists import seed_checklists
|
||||
seed_checklists()
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning("Checklist seed skipped: %s", e)
|
||||
# Планировщик рисков (передаём app для shutdown hook)
|
||||
setup_scheduler(app)
|
||||
yield
|
||||
|
||||
@ -47,6 +47,18 @@ class ChecklistTemplateOut(BaseModel):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ChecklistTemplateUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
domain: Optional[str] = None
|
||||
|
||||
|
||||
class ChecklistItemUpdate(BaseModel):
|
||||
code: Optional[str] = None
|
||||
text: Optional[str] = None
|
||||
sort_order: Optional[int] = None
|
||||
|
||||
|
||||
# --- Audit ---
|
||||
class AuditCreate(BaseModel):
|
||||
template_id: str
|
||||
|
||||
254
components/ChecklistEditModal.tsx
Normal file
254
components/ChecklistEditModal.tsx
Normal file
@ -0,0 +1,254 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { checklistsApi } from '@/lib/api/api-client';
|
||||
|
||||
export interface ChecklistEditModalTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
domain?: string | null;
|
||||
version: number;
|
||||
items: { id: string; code: string; text: string; sort_order: number }[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
template: ChecklistEditModalTemplate;
|
||||
onClose: () => void;
|
||||
onSaved: () => void;
|
||||
}
|
||||
|
||||
export default function ChecklistEditModal({ template, onClose, onSaved }: Props) {
|
||||
const [name, setName] = useState(template.name);
|
||||
const [description, setDescription] = useState(template.description ?? '');
|
||||
const [savingHeader, setSavingHeader] = useState(false);
|
||||
const [items, setItems] = useState(template.items);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editCode, setEditCode] = useState('');
|
||||
const [editText, setEditText] = useState('');
|
||||
const [newCode, setNewCode] = useState('');
|
||||
const [newText, setNewText] = useState('');
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
|
||||
const saveHeader = async () => {
|
||||
setSavingHeader(true);
|
||||
try {
|
||||
await checklistsApi.updateTemplate(template.id, {
|
||||
name: name.trim() || template.name,
|
||||
description: description.trim() || null,
|
||||
});
|
||||
onSaved();
|
||||
} finally {
|
||||
setSavingHeader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startEdit = (item: { id: string; code: string; text: string }) => {
|
||||
setEditingId(item.id);
|
||||
setEditCode(item.code);
|
||||
setEditText(item.text);
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
const saveEdit = async () => {
|
||||
if (!editingId) return;
|
||||
try {
|
||||
await checklistsApi.updateItem(editingId, { code: editCode.trim(), text: editText.trim() });
|
||||
setItems((prev) =>
|
||||
prev.map((i) => (i.id === editingId ? { ...i, code: editCode.trim(), text: editText.trim() } : i))
|
||||
);
|
||||
setEditingId(null);
|
||||
onSaved();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteItem = async (itemId: string) => {
|
||||
if (!confirm('Удалить пункт?')) return;
|
||||
setDeletingId(itemId);
|
||||
try {
|
||||
await checklistsApi.deleteItem(itemId);
|
||||
setItems((prev) => prev.filter((i) => i.id !== itemId));
|
||||
onSaved();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const addItem = async () => {
|
||||
const code = newCode.trim();
|
||||
const text = newText.trim();
|
||||
if (!code || !text) return;
|
||||
setAdding(true);
|
||||
try {
|
||||
const created = await checklistsApi.addItem(template.id, {
|
||||
code,
|
||||
text,
|
||||
domain: template.domain ?? undefined,
|
||||
sort_order: items.length + 1,
|
||||
});
|
||||
setItems((prev) => [...prev, created]);
|
||||
setNewCode('');
|
||||
setNewText('');
|
||||
onSaved();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setAdding(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
||||
<div
|
||||
className="flex max-h-[90vh] w-full max-w-4xl flex-col rounded-lg bg-white shadow-xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="border-b border-gray-200 p-4">
|
||||
<div className="mb-2">
|
||||
<label className="text-xs text-gray-500">Название</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full rounded border border-gray-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label className="text-xs text-gray-500">Описание</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={2}
|
||||
className="w-full rounded border border-gray-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={saveHeader}
|
||||
disabled={savingHeader}
|
||||
className="rounded bg-primary-500 px-3 py-1.5 text-sm text-white hover:bg-primary-600 disabled:opacity-50"
|
||||
>
|
||||
{savingHeader ? 'Сохранение…' : 'Сохранить заголовок'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="space-y-2">
|
||||
{items.map((item, idx) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="group flex items-start gap-2 rounded border border-gray-100 bg-gray-50/50 p-2"
|
||||
>
|
||||
<span className="w-6 shrink-0 text-xs text-gray-400">{idx + 1}</span>
|
||||
{editingId === item.id ? (
|
||||
<>
|
||||
<input
|
||||
value={editCode}
|
||||
onChange={(e) => setEditCode(e.target.value)}
|
||||
placeholder="Код"
|
||||
className="w-24 rounded border border-gray-300 px-2 py-1 text-sm"
|
||||
/>
|
||||
<input
|
||||
value={editText}
|
||||
onChange={(e) => setEditText(e.target.value)}
|
||||
placeholder="Текст"
|
||||
className="min-w-0 flex-1 rounded border border-gray-300 px-2 py-1 text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={saveEdit}
|
||||
className="rounded bg-green-600 px-2 py-1 text-xs text-white hover:bg-green-700"
|
||||
>
|
||||
Сохранить
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={cancelEdit}
|
||||
className="rounded bg-gray-400 px-2 py-1 text-xs text-white hover:bg-gray-500"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="min-w-[80px] shrink-0 text-xs font-medium text-primary-600">{item.code}</span>
|
||||
<span className="min-w-0 flex-1 text-sm text-gray-800">{item.text}</span>
|
||||
<div className="flex shrink-0 gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => startEdit(item)}
|
||||
className="rounded p-1 hover:bg-gray-200"
|
||||
title="Редактировать"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => deleteItem(item.id)}
|
||||
disabled={deletingId === item.id}
|
||||
className="rounded p-1 hover:bg-red-100 disabled:opacity-50"
|
||||
title="Удалить"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-end gap-2 rounded border border-dashed border-gray-300 bg-gray-50/50 p-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">Код</label>
|
||||
<input
|
||||
value={newCode}
|
||||
onChange={(e) => setNewCode(e.target.value)}
|
||||
placeholder="Напр. 148.16"
|
||||
className="ml-1 w-28 rounded border border-gray-300 px-2 py-1.5 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<label className="text-xs text-gray-500">Текст</label>
|
||||
<input
|
||||
value={newText}
|
||||
onChange={(e) => setNewText(e.target.value)}
|
||||
placeholder="Текст пункта"
|
||||
className="ml-1 w-full rounded border border-gray-300 px-2 py-1.5 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addItem}
|
||||
disabled={adding || !newCode.trim() || !newText.trim()}
|
||||
className="rounded bg-primary-500 px-3 py-1.5 text-sm text-white hover:bg-primary-600 disabled:opacity-50"
|
||||
>
|
||||
{adding ? '…' : 'Добавить'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-gray-200 px-4 py-3 text-sm text-gray-500">
|
||||
<span>
|
||||
{template.domain ?? '—'} · v{template.version} · {items.length} пунктов
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded bg-gray-200 px-3 py-1.5 hover:bg-gray-300"
|
||||
>
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -191,6 +191,14 @@ export const checklistsApi = {
|
||||
getTemplate: (id: string) => apiFetch(`/checklists/templates/${id}`),
|
||||
createTemplate: (data: any) => apiFetch('/checklists/templates', { method: 'POST', body: JSON.stringify(data) }),
|
||||
generate: (source: string, name: string, items?: any[]) => apiFetch(`/checklists/generate?source=${source}&name=${name}`, { method: 'POST', body: items ? JSON.stringify(items) : undefined }),
|
||||
updateTemplate: (id: string, data: any) =>
|
||||
apiFetch(`/checklists/templates/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
||||
addItem: (templateId: string, data: any) =>
|
||||
apiFetch(`/checklists/templates/${templateId}/items`, { method: 'POST', body: JSON.stringify(data) }),
|
||||
updateItem: (itemId: string, data: any) =>
|
||||
apiFetch(`/checklists/items/${itemId}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
||||
deleteItem: (itemId: string) =>
|
||||
apiFetch(`/checklists/items/${itemId}`, { method: 'DELETE' }),
|
||||
};
|
||||
|
||||
// Notifications
|
||||
|
||||
Loading…
Reference in New Issue
Block a user