klg-asutk-app/app/defects/page.tsx
Yuriy e9ef17ba16 KLG ASUTK: единый промт — персонал ПЛГ, ВС, заявки, аудиты, риски, пользователи, контроль ЛГ, ТО, дефекты, настройки, справка, логотип REFLY
- Персонал ПЛГ: демо-программы, таблица прохождений, кнопки Добавить/Редактировать/Экспорт
- ВС: AircraftEditModal, кнопка Редактировать в строке
- Заявки: Создать по шаблону, 5 демо-заявок
- Аудиты: AuditEditModal, чек-лист замечаний, auditsApi.update
- Риски: RiskDetailModal при клике, Скачать отчёт, Закрыть
- Пользователи: таблица с демо (10), CRUD, Экспорт, поиск и фильтры
- Контроль ЛГ: вкладка с 7 демо-записями, модалка, Скачать сертификат
- Тех. обслуживание: демо-наряды при ошибке API, Скачать наряд
- Дефекты: 7 демо-дефектов, детальный просмотр, цвет по серьёзности
- Настройки: Профиль организации, Шаблоны, Справочники, Экспорт/Импорт
- Справка: HelpDocumentModal при клике, оглавление, Скачать PDF
- ReflyLogo: компонент, Sidebar и страница логина

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 16:19:39 +03:00

153 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect, useCallback } from 'react';
import { PageLayout, DataTable, StatusBadge, Modal, EmptyState } from '@/components/ui';
const DEMO_DEFECTS = [
{ id: '1', defect_number: 'DEF-2024-001', aircraft_reg: 'RA-73001', ata_chapter: '32', description: 'Утечка гидравлической жидкости в районе стойки шасси', severity: 'critical', discovered_date: '2024-11-20', discovered_during: 'preflight', status: 'open', responsible: 'Иванов И.И.', corrective_actions: 'Замена уплотнений', mel_reference: '32-01-01', history: [{ date: '2024-11-20', action: 'Зарегистрирован', user: 'Иванов И.И.' }] },
{ id: '2', defect_number: 'DEF-2024-002', aircraft_reg: 'RA-73002', ata_chapter: '33', description: 'Нестабильные показания датчика освещённости', severity: 'major', discovered_date: '2024-11-18', discovered_during: 'daily', status: 'deferred', responsible: 'Петров П.П.', mel_reference: '33-02-01', history: [{ date: '2024-11-18', action: 'Зарегистрирован' }, { date: '2024-11-19', action: 'Отложен по MEL' }] },
{ id: '3', defect_number: 'DEF-2024-003', aircraft_reg: 'VQ-BAB', ata_chapter: '21', description: 'Потёртость уплотнения двери салона', severity: 'minor', discovered_date: '2024-11-15', discovered_during: 'a_check', status: 'rectified', responsible: 'Сидорова А.С.', corrective_actions: 'Замена уплотнения', history: [{ date: '2024-11-15', action: 'Зарегистрирован' }, { date: '2024-11-16', action: 'Устранён' }] },
{ id: '4', defect_number: 'DEF-2024-004', aircraft_reg: 'RA-73003', ata_chapter: '80', description: 'Повышенная вибрация двигателя №1', severity: 'critical', discovered_date: '2024-12-01', discovered_during: 'report', status: 'open', responsible: 'Козлов М.А.', corrective_actions: 'Диагностика, возможна замена двигателя', history: [{ date: '2024-12-01', action: 'Зарегистрирован по донесению экипажа' }] },
{ id: '5', defect_number: 'DEF-2024-005', aircraft_reg: 'RA-73005', ata_chapter: '27', description: 'Не работает подсветка панели', severity: 'minor', discovered_date: '2024-11-25', discovered_during: 'transit', status: 'closed', responsible: 'Новикова Е.В.', corrective_actions: 'Замена лампы', history: [{ date: '2024-11-25', action: 'Зарегистрирован' }, { date: '2024-11-26', action: 'Устранён' }, { date: '2024-11-26', action: 'Закрыт' }] },
{ id: '6', defect_number: 'DEF-2024-006', aircraft_reg: 'RA-73001', ata_chapter: '36', description: 'Срабатывание сигнализации давления в пневмосистеме', severity: 'major', discovered_date: '2024-11-28', discovered_during: 'preflight', status: 'open', responsible: 'Иванов И.И.', history: [{ date: '2024-11-28', action: 'Зарегистрирован' }] },
{ id: '7', defect_number: 'DEF-2024-007', aircraft_reg: 'RA-73006', ata_chapter: '52', description: 'Трещина остекления кабины (незначительная)', severity: 'minor', discovered_date: '2024-11-22', discovered_during: 'daily', status: 'rectified', responsible: 'Морозова Е.И.', corrective_actions: 'Замена остекления', history: [{ date: '2024-11-22', action: 'Зарегистрирован' }, { date: '2024-11-24', action: 'Устранён' }] },
];
const SEV_COLORS: Record<string, string> = { critical: 'bg-red-500', major: 'bg-orange-500', minor: 'bg-yellow-500' };
const SEV_LABELS: Record<string, string> = { critical: 'Критический', major: 'Значительный', minor: 'Незначительный' };
const STATUS_LABELS: Record<string, string> = { open: 'Открыт', deferred: 'Отложен (MEL)', rectified: 'Устранён', closed: 'Закрыт' };
export default function DefectsPage() {
const [defects, setDefects] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showAdd, setShowAdd] = useState(false);
const [filter, setFilter] = useState('');
const [selected, setSelected] = useState<any>(null);
const api = useCallback(async (ep: string, opts?: RequestInit) => {
const r = await fetch(`/api/v1/defects${ep}`, opts);
return r.json();
}, []);
useEffect(() => {
setLoading(true);
api(`/${filter ? `?status=${filter}` : ''}`)
.then(d => setDefects(Array.isArray(d?.items) && d.items.length > 0 ? d.items : DEMO_DEFECTS))
.catch(() => setDefects(DEMO_DEFECTS))
.finally(() => setLoading(false));
}, [api, filter]);
const displayDefects = filter ? defects.filter(d => d.status === filter) : defects;
const handleAdd = async (data: any) => {
const r = await api('/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
if (r.id) { setDefects(p => [r, ...p]); setShowAdd(false); }
};
const handleCloseDefect = (id: string) => {
if (!confirm('Закрыть дефект?')) return;
setDefects(prev => prev.map(d => d.id === id ? { ...d, status: 'closed' } : d));
setSelected(null);
};
const downloadReport = (d: any) => {
const lines = [`Отчёт по дефекту ${d.defect_number || d.id}`, `Борт: ${d.aircraft_reg}`, `ATA: ${d.ata_chapter}`, `Описание: ${d.description}`, `Серьёзность: ${SEV_LABELS[d.severity] || d.severity}`, `Дата обнаружения: ${d.discovered_date || '—'}`, `Статус: ${STATUS_LABELS[d.status]}`, `Ответственный: ${d.responsible || '—'}`, d.corrective_actions ? `Корректирующие действия: ${d.corrective_actions}` : '', 'История:', ...(d.history || []).map((h: any) => ` ${h.date}${h.action}`)];
const blob = new Blob([lines.filter(Boolean).join('\n')], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = `defect_${d.defect_number || d.id}.txt`; a.click();
URL.revokeObjectURL(url);
};
return (
<>
<PageLayout title="🛠️ Дефекты и неисправности" subtitle="ФАП-145 п.145.A.50; EASA Part-M.A.403"
actions={<button onClick={() => setShowAdd(true)} className="btn-primary text-sm px-4 py-2 rounded">+ Зарегистрировать</button>}>
<div className="flex gap-2 mb-4">
{['', 'open', 'deferred', 'rectified', 'closed'].map(s => (
<button key={s} onClick={() => setFilter(s)}
className={filter === s ? 'px-3 py-1.5 rounded text-xs bg-blue-600 text-white' : 'px-3 py-1.5 rounded text-xs bg-gray-100 text-gray-600'}>
{STATUS_LABELS[s] || 'Все'}
</button>
))}
</div>
{displayDefects.length > 0 ? (
<DataTable columns={[
{ key: 'defect_number', label: '№', render: (_: any, row: any) => row.defect_number || row.id },
{ key: 'aircraft_reg', label: 'Борт' },
{ key: 'ata_chapter', label: 'ATA' },
{ key: 'description', label: 'Описание', render: (v: string) => <span className="line-clamp-1">{v || '—'}</span> },
{ key: 'severity', label: 'Серьёзность', render: (v: string) => (
<StatusBadge status={v} colorMap={SEV_COLORS} labelMap={SEV_LABELS} />
)},
{ key: 'discovered_date', label: 'Дата', render: (v: string) => v ? new Date(v).toLocaleDateString('ru-RU') : '—' },
{ key: 'status', label: 'Статус', render: (v: string) => (
<StatusBadge status={v} colorMap={{ open: 'bg-red-500', deferred: 'bg-yellow-500', rectified: 'bg-green-500', closed: 'bg-gray-400' }} labelMap={STATUS_LABELS} />
)},
]} data={displayDefects} onRowClick={setSelected} />
) : <EmptyState message="Нет зарегистрированных дефектов" />}
<Modal isOpen={!!selected} onClose={() => setSelected(null)} title={selected ? `Дефект ${selected.defect_number || selected.id}` : ''} size="lg"
footer={selected ? (
<div className="flex gap-2">
<button onClick={() => setSelected(null)} className="btn-secondary">Редактировать</button>
<button onClick={() => downloadReport(selected)} className="btn-primary">Скачать отчёт</button>
{selected.status !== 'closed' && <button onClick={() => handleCloseDefect(selected.id)} className="btn-sm bg-green-600 text-white">Закрыть дефект</button>}
<button onClick={() => setSelected(null)} className="btn-secondary">Закрыть</button>
</div>
) : undefined}>
{selected && (
<div className="space-y-4 text-sm">
<div className={`p-2 rounded border-l-4 ${selected.severity === 'critical' ? 'bg-red-50 border-red-500' : selected.severity === 'major' ? 'bg-orange-50 border-orange-500' : 'bg-yellow-50 border-yellow-500'}`}>
<span className="font-medium">{SEV_LABELS[selected.severity] || selected.severity}</span>
</div>
<div><span className="text-gray-500">Описание</span><p className="mt-1">{selected.description}</p></div>
<div className="grid grid-cols-2 gap-3">
<div><span className="text-gray-500">Борт</span><div>{selected.aircraft_reg}</div></div>
<div><span className="text-gray-500">ATA</span><div>{selected.ata_chapter}</div></div>
<div><span className="text-gray-500">Дата обнаружения</span><div>{selected.discovered_date ? new Date(selected.discovered_date).toLocaleDateString('ru-RU') : '—'}</div></div>
<div><span className="text-gray-500">Ответственный</span><div>{selected.responsible || '—'}</div></div>
<div><span className="text-gray-500">MEL</span><div>{selected.mel_reference || '—'}</div></div>
</div>
{selected.corrective_actions && <div><span className="text-gray-500">Корректирующие действия</span><p className="mt-1">{selected.corrective_actions}</p></div>}
<div><span className="text-gray-500">Фото/вложения</span><p className="mt-1 text-gray-400 text-xs">Фото прилагаются к дефекту в системе учёта</p></div>
<div><h4 className="font-medium text-gray-700 mb-2">История</h4><ul className="space-y-1 text-gray-600">{(selected.history || []).map((h: any, i: number) => <li key={i}>{h.date} {h.action}{h.user ? ` (${h.user})` : ''}</li>)}</ul></div>
</div>
)}
</Modal>
<Modal isOpen={showAdd} onClose={() => setShowAdd(false)} title="Регистрация дефекта">
<DefectForm onSubmit={handleAdd} onCancel={() => setShowAdd(false)} />
</Modal>
</PageLayout>
</>
);
}
function DefectForm({ onSubmit, onCancel }: { onSubmit: (d: any) => void; onCancel: () => void }) {
const [f, setF] = useState({ aircraft_reg: '', ata_chapter: '', description: '', severity: 'minor', discovered_during: 'preflight' });
return (
<div className="space-y-3">
<div><label className="text-xs font-medium text-gray-600">Борт (рег. знак)</label>
<input className="input-field w-full mt-1" value={f.aircraft_reg} onChange={e => setF(p => ({ ...p, aircraft_reg: e.target.value }))} /></div>
<div><label className="text-xs font-medium text-gray-600">ATA Chapter</label>
<input className="input-field w-full mt-1" placeholder="32" value={f.ata_chapter} onChange={e => setF(p => ({ ...p, ata_chapter: e.target.value }))} /></div>
<div><label className="text-xs font-medium text-gray-600">Описание дефекта</label>
<textarea className="input-field w-full mt-1" rows={3} value={f.description} onChange={e => setF(p => ({ ...p, description: e.target.value }))} /></div>
<div className="grid grid-cols-2 gap-3">
<div><label className="text-xs font-medium text-gray-600">Серьёзность</label>
<select className="input-field w-full mt-1" value={f.severity} onChange={e => setF(p => ({ ...p, severity: e.target.value }))}>
<option value="critical">Критический</option><option value="major">Значительный</option><option value="minor">Незначительный</option>
</select></div>
<div><label className="text-xs font-medium text-gray-600">Обнаружен при</label>
<select className="input-field w-full mt-1" value={f.discovered_during} onChange={e => setF(p => ({ ...p, discovered_during: e.target.value }))}>
<option value="preflight">Предполётный</option><option value="transit">Транзит</option><option value="daily">Ежедневный</option>
<option value="a_check">A-check</option><option value="c_check">C-check</option><option value="report">Донесение экипажа</option>
</select></div>
</div>
<div className="flex gap-2 pt-2">
<button onClick={() => onSubmit(f)} className="btn-primary px-4 py-2 rounded text-sm">Сохранить</button>
<button onClick={onCancel} className="bg-gray-200 text-gray-700 px-4 py-2 rounded text-sm">Отмена</button>
</div>
</div>
);
}