diff --git a/app/aircraft/page.tsx b/app/aircraft/page.tsx index 465f5a2..9b6a181 100644 --- a/app/aircraft/page.tsx +++ b/app/aircraft/page.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { PageLayout, Pagination, StatusBadge, EmptyState } from '@/components/ui'; import AircraftAddModal from '@/components/AircraftAddModal'; +import AircraftEditModal from '@/components/AircraftEditModal'; import { useAircraftData } from '@/hooks/useSWRData'; import { aircraftApi } from '@/lib/api/api-client'; import { RequireRole } from '@/lib/auth-context'; @@ -10,6 +11,7 @@ export default function AircraftPage() { const [page, setPage] = useState(1); const [search, setSearch] = useState(''); const [isAddOpen, setIsAddOpen] = useState(false); + const [editingAircraft, setEditingAircraft] = useState(null); const { data, isLoading, mutate } = useAircraftData({ q: search || undefined, page, limit: 25 }); const aircraft = Array.isArray(data?.items) ? data.items : (Array.isArray(data) ? data : []); const total = data?.total ?? aircraft.length; @@ -22,6 +24,7 @@ export default function AircraftPage() { }; const handleAdd = async (d: any) => { try { await aircraftApi.create(d); mutate(); setIsAddOpen(false); } catch (e: any) { alert(e.message); } }; + const handleSave = async (id: string, d: any) => { try { await aircraftApi.update(id, d); mutate(); setEditingAircraft(null); } catch (e: any) { alert(e.message); } }; const handleDelete = async (id: string) => { if (!confirm('Удалить ВС?')) return; try { await aircraftApi.delete(id); mutate(); } catch (e: any) { alert(e.message); } }; return ( @@ -52,7 +55,12 @@ export default function AircraftPage() { - +
+ + +
@@ -64,6 +72,7 @@ export default function AircraftPage() { ) : } setIsAddOpen(false)} onAdd={handleAdd} /> + setEditingAircraft(null)} aircraft={editingAircraft} onSave={handleSave} /> ); } diff --git a/app/airworthiness-core/page.tsx b/app/airworthiness-core/page.tsx index ff19d9c..09f5c0c 100644 --- a/app/airworthiness-core/page.tsx +++ b/app/airworthiness-core/page.tsx @@ -8,9 +8,32 @@ import { useState, useEffect, useCallback } from 'react'; import { PageLayout, DataTable, StatusBadge, Modal, EmptyState } from '@/components/ui'; -type Tab = 'directives' | 'bulletins' | 'life-limits' | 'maint-programs' | 'components'; +type Tab = 'control' | 'directives' | 'bulletins' | 'life-limits' | 'maint-programs' | 'components'; + +interface ControlRecord { + id: string; + registration: string; + aircraft_type: string; + last_check_date: string; + status: string; + valid_until: string; + responsible: string; + notes?: string; + history?: { date: string; type: string; result: string }[]; +} + +const DEMO_CONTROL: ControlRecord[] = [ + { id: '1', registration: 'RA-73001', aircraft_type: 'SSJ-100', last_check_date: '2024-11-15', status: 'Годен', valid_until: '2025-11-15', responsible: 'Иванов И.И.', history: [{ date: '2024-11-15', type: 'Периодический осмотр', result: 'Годен' }, { date: '2023-11-10', type: 'Периодический осмотр', result: 'Годен' }] }, + { id: '2', registration: 'RA-73002', aircraft_type: 'MC-21', last_check_date: '2024-10-20', status: 'Годен', valid_until: '2025-10-20', responsible: 'Петров П.П.' }, + { id: '3', registration: 'RA-73003', aircraft_type: 'Ан-148', last_check_date: '2024-09-05', status: 'Ограниченно годен', valid_until: '2025-01-05', responsible: 'Сидорова А.С.', notes: 'Ограничение по наработке двигателя' }, + { id: '4', registration: 'VQ-BAB', aircraft_type: 'Boeing 737-800', last_check_date: '2024-12-01', status: 'Годен', valid_until: '2025-12-01', responsible: 'Козлов М.А.' }, + { id: '5', registration: 'RA-73005', aircraft_type: 'Airbus A320', last_check_date: '2024-08-12', status: 'Годен', valid_until: '2025-08-12', responsible: 'Новикова Е.В.' }, + { id: '6', registration: 'RA-73006', aircraft_type: 'SSJ-100', last_check_date: '2024-11-28', status: 'Годен', valid_until: '2025-11-28', responsible: 'Иванов И.И.' }, + { id: '7', registration: 'RA-73007', aircraft_type: 'MC-21', last_check_date: '2024-10-10', status: 'На проверке', valid_until: '—', responsible: 'Петров П.П.' }, +]; const TABS: { id: Tab; label: string; icon: string; basis: string }[] = [ + { id: 'control', label: 'Контроль ЛГ', icon: '✈️', basis: 'ВК РФ ст. 36; ФАП-148; Контроль лётной годности ВС' }, { id: 'directives', label: 'ДЛГ / AD', icon: '⚠️', basis: 'ВК РФ ст. 37; ФАП-148 п.4.3' }, { id: 'bulletins', label: 'Бюллетени SB', icon: '📢', basis: 'ФАП-148 п.4.5; EASA Part-21' }, { id: 'life-limits', label: 'Ресурсы', icon: '⏱️', basis: 'ФАП-148 п.4.2; EASA Part-M.A.302' }, @@ -19,10 +42,14 @@ const TABS: { id: Tab; label: string; icon: string; basis: string }[] = [ ]; export default function AirworthinessCorePage() { - const [tab, setTab] = useState('directives'); + const [tab, setTab] = useState('control'); const [data, setData] = useState>({}); const [loading, setLoading] = useState(false); const [showAddModal, setShowAddModal] = useState(false); + const [controlRecords, setControlRecords] = useState(DEMO_CONTROL); + const [selectedControl, setSelectedControl] = useState(null); + const [controlFilter, setControlFilter] = useState(''); + const [controlSort, setControlSort] = useState<'registration' | 'last_check_date' | 'status'>('registration'); const api = useCallback(async (endpoint: string, opts?: RequestInit) => { const res = await fetch(`/api/v1/airworthiness-core/${endpoint}`, opts); @@ -30,13 +57,40 @@ export default function AirworthinessCorePage() { }, []); useEffect(() => { + if (tab === 'control') { setLoading(false); return; } setLoading(true); const endpoint = tab === 'life-limits' ? 'life-limits' : tab === 'maint-programs' ? 'maintenance-programs' : tab; api(endpoint).then(d => { setData(prev => ({ ...prev, [tab]: d })); setLoading(false); }); }, [tab, api]); const currentTab = TABS.find(t => t.id === tab)!; - const items = data[tab]?.items || []; + const items = tab === 'control' ? [] : (data[tab]?.items || []); + + const filteredControl = controlRecords + .filter(r => !controlFilter || r.registration.toLowerCase().includes(controlFilter.toLowerCase()) || r.aircraft_type.toLowerCase().includes(controlFilter.toLowerCase()) || r.status.toLowerCase().includes(controlFilter.toLowerCase())) + .sort((a, b) => { + const va = a[controlSort], vb = b[controlSort]; + return String(va).localeCompare(String(vb), undefined, { numeric: true }); + }); + + const downloadCertificate = (r: ControlRecord) => { + const text = [ + 'СЕРТИФИКАТ ЛЁТНОЙ ГОДНОСТИ (выписка)', + `Бортовой номер: ${r.registration}`, + `Тип ВС: ${r.aircraft_type}`, + `Дата последней проверки: ${r.last_check_date}`, + `Статус ЛГ: ${r.status}`, + `Срок действия: ${r.valid_until}`, + `Ответственный: ${r.responsible}`, + r.notes ? `Примечания: ${r.notes}` : '', + '', + 'Документ сформирован системой КЛГ АСУ ТК. © АО «REFLY»', + ].filter(Boolean).join('\n'); + const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); a.href = url; a.download = `certificate_${r.registration}.txt`; a.click(); + URL.revokeObjectURL(url); + }; const statusColors: Record = { open: 'bg-red-500', complied: 'bg-green-500', incorporated: 'bg-green-500', @@ -74,6 +128,42 @@ export default function AirworthinessCorePage() { {loading ?
⏳ Загрузка...
: ( <> + {/* CONTROL LG */} + {tab === 'control' && ( + <> +
+ setControlFilter(e.target.value)} className="input-field w-64" /> + Сортировка: + {(['registration', 'last_check_date', 'status'] as const).map(k => ( + + ))} +
+
+ + + + + + + {filteredControl.map(r => ( + setSelectedControl(r)} className="border-b border-gray-100 hover:bg-blue-50 cursor-pointer"> + + + + + + + + ))} + +
Бортовой номерТип ВСДата последней проверкиСтатус ЛГСрок действияОтветственный
{r.registration}{r.aircraft_type}{r.last_check_date}{r.status}{r.valid_until}{r.responsible}
+
+ {filteredControl.length === 0 && } + + )} + {/* DIRECTIVES (AD/ДЛГ) */} {tab === 'directives' && ( items.length > 0 ? ( @@ -165,6 +255,37 @@ export default function AirworthinessCorePage() { )} + {/* Control LG detail modal */} + setSelectedControl(null)} title={selectedControl ? `Контроль ЛГ — ${selectedControl.registration}` : ''} size="md" + footer={selectedControl ? ( +
+ + + +
+ ) : undefined}> + {selectedControl && ( +
+
+
Бортовой номер
{selectedControl.registration}
+
Тип ВС
{selectedControl.aircraft_type}
+
Дата последней проверки
{selectedControl.last_check_date}
+
Статус ЛГ
{selectedControl.status}
+
Срок действия
{selectedControl.valid_until}
+
Ответственный
{selectedControl.responsible}
+
+ {selectedControl.notes &&
Примечания

{selectedControl.notes}

} +
+

История проверок

+
    + {(selectedControl.history || []).map((h, i) =>
  • {h.date} — {h.type}: {h.result}
  • )} + {(!selectedControl.history || selectedControl.history.length === 0) &&
  • Нет данных
  • } +
+
+
+ )} + + {/* Legal basis footer */}
{currentTab.basis} · © АО «REFLY» diff --git a/app/applications/page.tsx b/app/applications/page.tsx index 79bd54a..80f82cc 100644 --- a/app/applications/page.tsx +++ b/app/applications/page.tsx @@ -1,29 +1,98 @@ 'use client'; import { useState, useEffect } from 'react'; -import { PageLayout, DataTable, StatusBadge, EmptyState } from '@/components/ui'; +import { PageLayout, DataTable, StatusBadge, EmptyState, Modal } from '@/components/ui'; + +const DEMO_APPLICATIONS = [ + { id: '1', number: 'ЗАЯВ-2024-001', type: 'Сертификат эксплуатанта', organization_name: 'АО «Авиакомпания»', aircraft_id: 'RA-73001', basis: 'ФАП-246', submitted_at: '2024-10-15', status: 'pending', attachments: 'Устав, свидетельство ВС' }, + { id: '2', number: 'ЗАЯВ-2024-002', type: 'Дополнение к сертификату', organization_name: 'ООО «АвиаСервис»', aircraft_id: 'RA-73002', basis: 'ФАП-246 п. 12', submitted_at: '2024-11-01', status: 'draft', attachments: 'Регламент ТО' }, + { id: '3', number: 'ЗАЯВ-2024-003', type: 'Сертификат эксплуатанта', organization_name: 'ПАО «Авиалинии»', aircraft_id: 'VQ-BAB', basis: 'ФАП-246; EASA Part-ORO', submitted_at: '2024-09-20', status: 'approved', attachments: 'Полный пакет' }, + { id: '4', number: 'ЗАЯВ-2024-004', type: 'Сертификат на тип ВС', organization_name: 'АО «Авиакомпания»', aircraft_id: 'RA-73003', basis: 'ФАП-21', submitted_at: '2024-11-10', status: 'pending', attachments: 'Заключение по типу' }, + { id: '5', number: 'ЗАЯВ-2024-005', type: 'Продление срока действия', organization_name: 'ООО «АвиаСервис»', aircraft_id: 'RA-73002', basis: 'ФАП-246', submitted_at: '2024-08-05', status: 'approved', attachments: 'Акт проверки' }, +]; + +const TEMPLATE_DEFAULT = { + type: 'Сертификат эксплуатанта', + organization_name: 'АО «Авиакомпания»', + aircraft_id: 'RA-73001', + basis: 'ФАП-246; ВК РФ ст. 36', + submitted_at: new Date().toISOString().slice(0, 10), + attachments: 'Устав, свидетельство о гос. регистрации, регламент ТО, список ВС', +}; export default function ApplicationsPage() { const [apps, setApps] = useState([] as any[]); const [loading, setLoading] = useState(true); + const [templateOpen, setTemplateOpen] = useState(false); + const [form, setForm] = useState(TEMPLATE_DEFAULT); + useEffect(() => { - setLoading(true); fetch('/api/v1/cert-applications').then(r => r.json()).then(d => { setApps(d.items || []); setLoading(false); }); }, []); + setLoading(true); + fetch('/api/v1/cert-applications') + .then(r => r.json()) + .then(d => { setApps(Array.isArray(d?.items) && d.items.length > 0 ? d.items : DEMO_APPLICATIONS); setLoading(false); }) + .catch(() => { setApps(DEMO_APPLICATIONS); setLoading(false); }); + }, []); + + const handleCreateFromTemplate = () => { + const newApp = { + id: String(Date.now()), + number: `ЗАЯВ-${new Date().getFullYear()}-${String(apps.length + 1).padStart(3, '0')}`, + ...form, + submitted_at: form.submitted_at, + status: 'draft', + }; + setApps(prev => [newApp, ...prev]); + setTemplateOpen(false); + setForm(TEMPLATE_DEFAULT); + }; + return ( <> - {loading &&
⏳ Загрузка...
} - - {apps.length > 0 ? ( - ( - - )}, - { key: 'submitted_at', label: 'Дата', render: (v: string) => v ? new Date(v).toLocaleDateString('ru-RU') : '—' }, - ]} data={apps} /> - ) : } - + {loading &&
⏳ Загрузка...
} + { setForm(TEMPLATE_DEFAULT); setTemplateOpen(true); }} className="btn-primary">Создать по шаблону} + > + {apps.length > 0 ? ( + v || '—' }, + { key: 'status', label: 'Статус', render: (v: string) => ( + + )}, + { key: 'submitted_at', label: 'Дата', render: (v: string) => v ? new Date(v).toLocaleDateString('ru-RU') : '—' }, + ]} + data={apps} + /> + ) : } + + + setTemplateOpen(false)} + title="Создать заявку по шаблону" + size="md" + footer={ + <> + + + + } + > +
+
setForm(f => ({ ...f, type: e.target.value }))} />
+
setForm(f => ({ ...f, organization_name: e.target.value }))} />
+
setForm(f => ({ ...f, aircraft_id: e.target.value }))} placeholder="RA-73001" />
+
setForm(f => ({ ...f, basis: e.target.value }))} />
+
setForm(f => ({ ...f, submitted_at: e.target.value }))} />
+