/** * Сертификация персонала ПЛГ * Учёт специалистов, первичная аттестация, повышение квалификации. * * Правовые основания: * ВК РФ ст. 52-54; ФАП-147; ФАП-145; ФАП-148 * EASA Part-66, Part-145.A.30/35, Part-CAMO.A.305 * ICAO Annex 1, Doc 9760 ch.6 */ 'use client'; import { useState, useEffect, useCallback } from 'react'; import { PageLayout, DataTable, StatusBadge, Modal, EmptyState } from '@/components/ui'; type Tab = 'specialists' | 'programs' | 'attestations' | 'compliance'; interface Specialist { id: string; full_name: string; personnel_number: string; position: string; category: string; specializations: string[]; license_number?: string; license_expires?: string; status: string; compliance?: any; attestations?: any[]; qualifications?: any[]; } interface Program { id: string; name: string; type: string; legal_basis: string; duration_hours: number; modules?: any[]; periodicity?: string; certificate_validity_years?: number; } export default function PersonnelPLGPage() { const [tab, setTab] = useState('specialists'); const [specialists, setSpecialists] = useState([]); const [programs, setPrograms] = useState([]); const [compliance, setCompliance] = useState(null); const [selected, setSelected] = useState(null); const [selectedProgram, setSelectedProgram] = useState(null); const [showAddModal, setShowAddModal] = useState(false); const [loading, setLoading] = useState(false); const api = useCallback(async (endpoint: string, opts?: RequestInit) => { const res = await fetch(`/api/v1/personnel-plg/${endpoint}`, opts); return res.json(); }, []); useEffect(() => { setLoading(true); Promise.all([ api('specialists').then(d => setSpecialists(d.items || [])), api('programs').then(d => setPrograms(d.programs || [])), api('compliance-report').then(d => setCompliance(d)), ]).finally(() => setLoading(false)); }, [api]); const handleAddSpecialist = async (data: any) => { const result = await api('specialists', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (result.id) { setSpecialists(prev => [...prev, result]); setShowAddModal(false); } }; const tabs = [ { id: 'specialists' as Tab, label: '👤 Специалисты', icon: '👤' }, { id: 'programs' as Tab, label: '📚 Программы подготовки', icon: '📚' }, { id: 'attestations' as Tab, label: '📝 Аттестация / ПК', icon: '📝' }, { id: 'compliance' as Tab, label: '✅ Соответствие', icon: '✅' }, ]; const programTypeLabels: Record = { initial: '🎓 Первичная', recurrent: '🔄 Периодическая', type_rating: '✈️ На тип ВС', ewis: '⚡ EWIS', fuel_tank: '⛽ FTS', ndt: '🔬 НК/NDT', human_factors: '🧠 ЧФ', sms: '🛡️ SMS', crs_authorization: '✍️ CRS', rvsm: '📏 RVSM', etops: '🌊 ETOPS', }; return ( setShowAddModal(true)} className="btn-primary text-sm px-4 py-2 rounded"> + Добавить специалиста }>
Правовая база: ВК РФ ст. 52-54; ФАП-147; ФАП-145 п.145.A.30/35; ФАП-148; EASA Part-66; Part-CAMO.A.305; ICAO Annex 1; Doc 9760 ch.6
{/* Tabs */}
{tabs.map(t => ( ))}
{loading ?
⏳ Загрузка...
: ( <> {/* SPECIALISTS */} {tab === 'specialists' && ( specialists.length > 0 ? ( {v} }, { key: 'specializations', label: 'Типы ВС', render: (v: string[]) => v?.join(', ') || '—' }, { key: 'license_number', label: 'Свидетельство' }, { key: 'status', label: 'Статус', render: (v: string) => ( )}, ]} data={specialists} onRowClick={(row) => { api(`specialists/${row.id}`).then(setSelected); }} /> ) : )} {/* PROGRAMS */} {tab === 'programs' && (
{programs.map(p => (
setSelectedProgram(p)} className="card p-4 cursor-pointer hover:shadow-md transition-shadow">
{programTypeLabels[p.type]?.split(' ')[0] || '📋'} {p.name}
{p.legal_basis}
{p.duration_hours} ч.
{p.periodicity &&
{p.periodicity}
} {p.certificate_validity_years ? (
Срок: {p.certificate_validity_years} лет
) : null}
))}
)} {/* ATTESTATIONS */} {tab === 'attestations' && (
🎓
Первичная аттестация
PLG-INIT-001 · 240 ч.
🔄
Периодическая ПК
PLG-REC-001 · 40 ч. · каждые 24 мес.
✈️
Допуск на тип ВС
PLG-TYPE-001 · 80 ч.

Специальные курсы

{['PLG-EWIS-001', 'PLG-FUEL-001', 'PLG-NDT-001', 'PLG-HF-001', 'PLG-SMS-001', 'PLG-CRS-001', 'PLG-RVSM-001', 'PLG-ETOPS-001'].map(pid => { const p = programs.find(pp => pp.id === pid); if (!p) return null; return (
setSelectedProgram(p)} className="card p-3 cursor-pointer hover:shadow-sm transition-shadow">
{programTypeLabels[p.type] || p.type}
{p.duration_hours} ч.
); })}
)} {/* COMPLIANCE */} {tab === 'compliance' && compliance && (
{compliance.total_specialists}
Всего специалистов
{compliance.compliant}
Соответствуют
{compliance.non_compliant}
Нарушения
{compliance.expiring_soon?.length || 0}
Истекает <90 дн.
{compliance.overdue?.length > 0 && (

⚠️ Просроченные квалификации

{compliance.overdue.map((o: any, i: number) => (
{o.specialist} {o.program} {new Date(o.due).toLocaleDateString('ru-RU')}
))}
)} {compliance.expiring_soon?.length > 0 && (

⏰ Истекает в течение 90 дней

{compliance.expiring_soon.map((e: any, i: number) => (
{e.specialist} {e.program || e.item} {new Date(e.due).toLocaleDateString('ru-RU')}
))}
)}
)} )} {/* Specialist detail modal */} setSelected(null)} title={selected?.full_name || ''} size="lg"> {selected && (
Таб. №: {selected.personnel_number}
Категория: {selected.category}
Должность: {selected.position}
Свидетельство: {selected.license_number || '—'}
Типы ВС: {selected.specializations?.join(', ') || '—'}
Действует до: {selected.license_expires ? new Date(selected.license_expires).toLocaleDateString('ru-RU') : '—'}
{selected.compliance && (
{selected.compliance.status === 'compliant' ? '✅ Все квалификации в порядке' : `⚠️ Просрочено: ${selected.compliance.overdue_items?.join(', ')}`}
)} {selected.attestations?.length > 0 && (

Аттестации

{selected.attestations.map((a: any) => (
{a.program_name}{a.result}
))}
)} {selected.qualifications?.length > 0 && (

Повышение квалификации

{selected.qualifications.map((q: any) => (
{q.program_name}{q.next_due ? `до ${new Date(q.next_due).toLocaleDateString('ru-RU')}` : '—'}
))}
)}
)}
{/* Program detail modal */} setSelectedProgram(null)} title={selectedProgram?.name || ''} size="lg"> {selectedProgram && (
{selectedProgram.legal_basis}
{selectedProgram.duration_hours} часов {selectedProgram.periodicity && {selectedProgram.periodicity}} {selectedProgram.certificate_validity_years ? Срок: {selectedProgram.certificate_validity_years} лет : null}
{selectedProgram.modules && (

Модули программы

{selectedProgram.modules.map((m: any, i: number) => (
{m.code}{m.name}
{m.hours > 0 && {m.hours}ч} {m.basis && {m.basis}}
))}
)}
)}
{/* Add specialist modal */} setShowAddModal(false)} title="Добавить специалиста ПЛГ" size="lg"> setShowAddModal(false)} />
); } function AddSpecialistForm({ onSubmit, onCancel }: { onSubmit: (d: any) => void; onCancel: () => void }) { const [form, setForm] = useState({ full_name: '', personnel_number: '', position: 'Авиатехник', category: 'B1', specializations: '', license_number: '' }); return (
{[ { key: 'full_name', label: 'ФИО', placeholder: 'Иванов Иван Иванович' }, { key: 'personnel_number', label: 'Табельный номер', placeholder: 'ТН-001' }, { key: 'position', label: 'Должность', placeholder: 'Авиатехник' }, { key: 'license_number', label: 'Номер свидетельства', placeholder: 'АС-12345' }, { key: 'specializations', label: 'Типы ВС (через запятую)', placeholder: 'Ан-148, SSJ-100' }, ].map(f => (
setForm(prev => ({ ...prev, [f.key]: e.target.value }))} />
))}
); }