diff --git a/app/checklists/page.tsx b/app/checklists/page.tsx index 98f909b..5e42ab6 100644 --- a/app/checklists/page.tsx +++ b/app/checklists/page.tsx @@ -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(null); + const [editTemplate, setEditTemplate] = useState(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() { }> + 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 ? (
{templates.map((t: any) => (
setExp(exp === t.id ? null : t.id)}>
{t.name}
{t.domain || '—'} · v{t.version || 1} · {t.items?.length || 0} пунктов
- {exp === t.id ? '▼' : '▶'} +
e.stopPropagation()}> + + + + {exp === t.id ? '▼' : '▶'} +
{exp === t.id && t.items?.length > 0 && (
@@ -45,6 +61,13 @@ export default function ChecklistsPage() { ))}
) : !isLoading ? : null} + {editTemplate && ( + setEditTemplate(null)} + onSaved={() => mutate()} + /> + )} ); } diff --git a/backend/app/api/routes/checklists.py b/backend/app/api/routes/checklists.py index 423affd..e7ad565 100644 --- a/backend/app/api/routes/checklists.py +++ b/backend/app/api/routes/checklists.py @@ -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, diff --git a/backend/app/demo/__init__.py b/backend/app/demo/__init__.py new file mode 100644 index 0000000..a460311 --- /dev/null +++ b/backend/app/demo/__init__.py @@ -0,0 +1 @@ +# Demo: seed и утилиты для demo-развёртывания diff --git a/backend/app/demo/seed.py b/backend/app/demo/seed.py new file mode 100644 index 0000000..d57e76e --- /dev/null +++ b/backend/app/demo/seed.py @@ -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() diff --git a/backend/app/demo/seed_checklists.py b/backend/app/demo/seed_checklists.py new file mode 100644 index 0000000..442bf00 --- /dev/null +++ b/backend/app/demo/seed_checklists.py @@ -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() diff --git a/backend/app/main.py b/backend/app/main.py index 32b4f68..1116e1c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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 diff --git a/backend/app/schemas/audit.py b/backend/app/schemas/audit.py index 8086e9f..63623b9 100644 --- a/backend/app/schemas/audit.py +++ b/backend/app/schemas/audit.py @@ -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 diff --git a/components/ChecklistEditModal.tsx b/components/ChecklistEditModal.tsx new file mode 100644 index 0000000..5fd6afa --- /dev/null +++ b/components/ChecklistEditModal.tsx @@ -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(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(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 ( +
+
e.stopPropagation()} + > +
+
+ + setName(e.target.value)} + className="w-full rounded border border-gray-300 px-3 py-2 text-sm" + /> +
+
+ +