import React, { useState, useEffect } from 'react'; import { Wrench, Plus, Search, Clock, CheckCircle2, XCircle, AlertCircle, X, Edit, Receipt, DollarSign, FileText, Calendar, AlertTriangle, Truck, Package, UserCog, Settings } from 'lucide-react'; import { OfficeRepairRequest, OfficeEquipment, RepairRequestStatus } from '../../types'; import { CURRENT_USER_MOCK } from '../../constants'; import { PaymentInvoiceForm } from '../finance/PaymentInvoiceForm'; import { apiClient, authFetch } from '../../services/apiClient'; import type { PaymentInvoice } from '../../types'; const isOverdue = (req: OfficeRepairRequest): boolean => { if (req.status === 'completed' || req.status === 'canceled') return false; if (!req.expectedReturnDate) return false; const expected = new Date(req.expectedReturnDate); const today = new Date(); today.setHours(0, 0, 0, 0); expected.setHours(0, 0, 0, 0); return expected < today; }; export const RepairRequests: React.FC = () => { const [requests, setRequests] = useState([]); const [equipment, setEquipment] = useState([]); const [loading, setLoading] = useState(true); const [showCreateModal, setShowCreateModal] = useState(false); const [filters, setFilters] = useState({ status: '' as RepairRequestStatus | '', equipmentId: '', search: '' }); const [formData, setFormData] = useState({ equipmentId: '', description: '', priority: 'medium' as 'low' | 'medium' | 'high' | 'urgent', expectedReturnDate: '' }); const [showCardModal, setShowCardModal] = useState(false); const [showEditRepairModal, setShowEditRepairModal] = useState(false); const [selectedRepair, setSelectedRepair] = useState(null); const [repairFormData, setRepairFormData] = useState({ status: '' as RepairRequestStatus | '', assignedTo: '', solution: '', expectedReturnDate: '', isPaid: false, cost: 0, costEstimated: false }); useEffect(() => { fetchEquipment(); }, []); useEffect(() => { fetchRequests(); }, [filters.status, filters.equipmentId]); const fetchRequests = async () => { try { const params = new URLSearchParams(); if (filters.status) params.append('status', filters.status); if (filters.equipmentId) params.append('equipmentId', filters.equipmentId); const response = await authFetch(`/api/office/repair-requests?${params}`); if (response.ok) { const data = await response.json(); // Нормализуем данные из API const normalizedData = data.map((req: any) => ({ id: req.id, equipmentId: req.equipment_id || req.equipmentId, equipment: req.equipment_name ? { id: req.equipment_id, name: req.equipment_name, type: req.equipment_type } : req.equipment, requesterName: req.requester_name || req.requesterName || '', description: req.description || '', priority: req.priority || 'medium', status: req.status || 'new', assignedTo: req.assigned_to || req.assignedTo, solution: req.solution, expectedReturnDate: req.expected_return_date || req.expectedReturnDate, waitingDeliveryDeadline: req.waiting_delivery_deadline || req.waitingDeliveryDeadline, waitingDeliveryContacts: req.waiting_delivery_contacts || req.waitingDeliveryContacts, takenForRepairDeadline: req.taken_for_repair_deadline || req.takenForRepairDeadline, takenForRepairContacts: req.taken_for_repair_contacts || req.takenForRepairContacts, agreedContractorPrice: req.agreed_contractor_price != null ? req.agreed_contractor_price : req.agreedContractorPrice, isPaid: req.is_paid || req.isPaid || false, cost: req.cost || 0, costEstimated: req.cost_estimated || req.costEstimated || false, invoiceId: req.invoice_id || req.invoiceId, invoiceUrl: req.invoice_url || req.invoiceUrl, createdAt: req.created_at || req.createdAt, startedAt: req.started_at || req.startedAt, completedAt: req.completed_at || req.completedAt, comments: Array.isArray(req.comments) ? req.comments.map((c: any) => ({ author: c.author || c.author_name || '', text: c.text || c.comment || '', createdAt: c.created_at || c.createdAt || '' })) : [] })); setRequests(normalizedData); } } catch (error) { console.error('Ошибка загрузки заявок:', error); } finally { setLoading(false); } }; const fetchEquipment = async () => { try { const response = await authFetch('/api/office/equipment'); if (response.ok) { const data = await response.json(); // Нормализуем данные из API const normalizedData = data.map((eq: any) => ({ id: eq.id, name: eq.name || '', type: eq.type || 'other', brand: eq.brand, model: eq.model, serialNumber: eq.serial_number || eq.serialNumber || '', assignedTo: eq.assigned_to || eq.assignedTo, purchaseDate: eq.purchase_date || eq.purchaseDate, warrantyUntil: eq.warranty_until || eq.warrantyUntil, nextMaintenanceDate: eq.next_maintenance_date || eq.nextMaintenanceDate, condition: eq.condition || 'good', notes: eq.notes, createdAt: eq.created_at, updatedAt: eq.updated_at })); setEquipment(normalizedData); } } catch (error) { console.error('Ошибка загрузки оборудования:', error); } }; const handleCreateRequest = async () => { try { if (!formData.equipmentId) { alert('Пожалуйста, выберите оборудование'); return; } if (!formData.description || !formData.description.trim()) { alert('Пожалуйста, укажите описание проблемы'); return; } const response = await authFetch('/api/office/repair-requests', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ equipmentId: parseInt(formData.equipmentId), requesterName: CURRENT_USER_MOCK.name, description: formData.description, priority: formData.priority || 'medium', expectedReturnDate: formData.expectedReturnDate || null }) }); if (response.ok) { const newRequest = await response.json(); // Нормализуем данные const normalizedRequest = { id: newRequest.id, equipmentId: newRequest.equipment_id, equipment: equipment.find(eq => eq.id === parseInt(formData.equipmentId)), requesterName: newRequest.requester_name || CURRENT_USER_MOCK.name, description: newRequest.description || formData.description, priority: newRequest.priority || formData.priority, status: newRequest.status || 'new', assignedTo: newRequest.assigned_to, solution: newRequest.solution, createdAt: newRequest.created_at, startedAt: newRequest.started_at, completedAt: newRequest.completed_at }; setRequests([normalizedRequest, ...requests]); setShowCreateModal(false); setFormData({ equipmentId: '', description: '', priority: 'medium', expectedReturnDate: '' }); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка создания заявки: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка создания заявки:', error); alert(`Ошибка создания заявки: ${error.message || 'Неизвестная ошибка'}`); } }; const REPAIR_STATUSES: { value: RepairRequestStatus; label: string; icon: React.ReactNode; bg: string }[] = [ { value: 'new', label: 'Новая', icon: , bg: 'text-blue-600' }, { value: 'search_contractor', label: 'Поиск подрядчика', icon: , bg: 'text-violet-600' }, { value: 'agreed_with_contractor', label: 'Договорились с подрядчиком', icon: , bg: 'text-indigo-600' }, { value: 'waiting_delivery', label: 'Ожидание поставки', icon: , bg: 'text-cyan-600' }, { value: 'taken_for_repair', label: 'Увезли на ремонт', icon: , bg: 'text-orange-600' }, { value: 'self_repair', label: 'Ремонт самостоятельно', icon: , bg: 'text-slate-600' }, { value: 'in_progress', label: 'В работе', icon: , bg: 'text-amber-600' }, { value: 'completed', label: 'Выполнена', icon: , bg: 'text-emerald-600' }, { value: 'canceled', label: 'Отменена', icon: , bg: 'text-red-600' } ]; const STATUSES_NEED_EXTRA = ['waiting_delivery', 'taken_for_repair', 'agreed_with_contractor'] as const; const [statusExtraModal, setStatusExtraModal] = useState<{ open: boolean; request: OfficeRepairRequest | null; newStatus: RepairRequestStatus | null }>({ open: false, request: null, newStatus: null }); const [statusExtraForm, setStatusExtraForm] = useState<{ deadline: string; contacts: string; price: string }>({ deadline: '', contacts: '', price: '' }); const [showInvoiceModal, setShowInvoiceModal] = useState(false); const [invoiceModalRepair, setInvoiceModalRepair] = useState(null); const [cardCommentText, setCardCommentText] = useState(''); const getStatusIcon = (status: RepairRequestStatus) => { const s = REPAIR_STATUSES.find((x) => x.value === status); return s ? {s.icon} : ; }; const getStatusLabel = (status: RepairRequestStatus) => { const s = REPAIR_STATUSES.find((x) => x.value === status); return s ? s.label : status; }; const setStatusQuick = async (request: OfficeRepairRequest, newStatus: RepairRequestStatus, e?: React.MouseEvent, extra?: Record) => { e?.stopPropagation(); try { const body: Record = { status: newStatus, ...extra }; const res = await authFetch(`/api/office/repair-requests/${request.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (res.ok) { fetchRequests(); if (selectedRepair?.id === request.id) { setSelectedRepair({ ...request, status: newStatus, ...extra } as OfficeRepairRequest); } } else { const data = await res.json().catch(() => ({})); alert(data?.error || `Ошибка смены статуса (${res.status})`); } } catch (err) { console.error(err); alert('Ошибка смены статуса'); } }; const handleQuickStatusChange = (request: OfficeRepairRequest, newStatus: RepairRequestStatus, e: React.ChangeEvent) => { e.stopPropagation(); if (STATUSES_NEED_EXTRA.includes(newStatus as any)) { setStatusExtraModal({ open: true, request, newStatus }); setStatusExtraForm({ deadline: newStatus === 'waiting_delivery' ? (request.waitingDeliveryDeadline || '') : (request.takenForRepairDeadline || ''), contacts: newStatus === 'waiting_delivery' ? (request.waitingDeliveryContacts || '') : (request.takenForRepairContacts || ''), price: request.agreedContractorPrice != null ? String(request.agreedContractorPrice) : '' }); e.currentTarget.value = ''; return; } setStatusQuick(request, newStatus, e as unknown as React.MouseEvent); e.currentTarget.value = ''; }; const submitStatusExtra = async () => { if (!statusExtraModal.request || !statusExtraModal.newStatus) return; const { request, newStatus } = statusExtraModal; const extra: Record = {}; if (newStatus === 'waiting_delivery') { extra.waitingDeliveryDeadline = statusExtraForm.deadline || null; extra.waitingDeliveryContacts = statusExtraForm.contacts || null; } else if (newStatus === 'taken_for_repair') { extra.takenForRepairDeadline = statusExtraForm.deadline || null; extra.takenForRepairContacts = statusExtraForm.contacts || null; } else if (newStatus === 'agreed_with_contractor') { extra.agreedContractorPrice = statusExtraForm.price ? parseFloat(statusExtraForm.price) : null; } await setStatusQuick(request, newStatus, undefined, extra); setStatusExtraModal({ open: false, request: null, newStatus: null }); setStatusExtraForm({ deadline: '', contacts: '', price: '' }); }; const filteredRequests = requests.filter(req => { if (filters.search) { const searchLower = filters.search.toLowerCase(); const description = (req.description || '').toLowerCase(); const requesterName = (req.requesterName || '').toLowerCase(); const equipmentName = (req.equipment?.name || '').toLowerCase(); return ( description.includes(searchLower) || requesterName.includes(searchLower) || equipmentName.includes(searchLower) ); } return true; }); return (

Заявки на ремонт техники

{/* Фильтры */}
setFilters({ ...filters, search: e.target.value })} placeholder="Поиск..." className="w-full pl-10 pr-3 py-2 border border-slate-300 rounded-lg text-sm" />
{/* Список заявок */} {loading ? (
) : filteredRequests.length > 0 ? (
{filteredRequests.map((request) => (
{ setSelectedRepair(request); setShowCardModal(true); }} >
{getStatusIcon(request.status)} {request.equipment?.name || 'Оборудование не указано'} {request.createdAt ? new Date(request.createdAt).toLocaleDateString('ru-RU') : 'Дата не указана'} {request.expectedReturnDate && ( Вернуть до: {new Date(request.expectedReturnDate).toLocaleDateString('ru-RU')} )} {isOverdue(request) && ( Просрочено )}

{request.description || 'Описание не указано'}

Заявитель: {request.requesterName || 'Не указан'} {request.assignedTo && Исполнитель: {request.assignedTo}} {request.isPaid && request.cost && request.cost > 0 && ( {request.cost} ₽{request.costEstimated ? ' (предв.)' : ''} )} {request.priority === 'urgent' ? 'Срочно' : request.priority === 'high' ? 'Высокий' : request.priority === 'medium' ? 'Средний' : 'Низкий'}
e.stopPropagation()}> {getStatusLabel(request.status)} {request.status === 'waiting_delivery' && (request.waitingDeliveryDeadline || request.waitingDeliveryContacts) && ( {request.waitingDeliveryDeadline && Срок: {request.waitingDeliveryDeadline}} {request.waitingDeliveryDeadline && request.waitingDeliveryContacts && ' · '} {request.waitingDeliveryContacts && Контакты: {request.waitingDeliveryContacts}} )} {request.status === 'taken_for_repair' && (request.takenForRepairDeadline || request.takenForRepairContacts) && ( {request.takenForRepairDeadline && Срок: {request.takenForRepairDeadline}} {request.takenForRepairDeadline && request.takenForRepairContacts && ' · '} {request.takenForRepairContacts && Контакты: {request.takenForRepairContacts}} )} {request.status === 'agreed_with_contractor' && request.agreedContractorPrice != null && request.agreedContractorPrice > 0 && ( Цена: {request.agreedContractorPrice} ₽ )}
{!request.invoiceId && (['waiting_delivery', 'taken_for_repair', 'agreed_with_contractor', 'in_progress'].includes(request.status) || (request.isPaid && (request.cost || 0) > 0)) && ( )}
))}
) : (

Нет заявок на ремонт

)} {/* Карточка ремонта */} {showCardModal && selectedRepair && (
{ setShowCardModal(false); setCardCommentText(''); }}>
e.stopPropagation()}>

Карточка заявки на ремонт

{getStatusIcon(selectedRepair.status)} {getStatusLabel(selectedRepair.status)} {isOverdue(selectedRepair) && ( Просрочено )} {selectedRepair.priority === 'urgent' ? 'Срочно' : selectedRepair.priority === 'high' ? 'Высокий' : selectedRepair.priority === 'medium' ? 'Средний' : 'Низкий'}

Быстрый статус

Оборудование

{selectedRepair.equipment?.name || '—'}

Описание проблемы

{selectedRepair.description || '—'}

Заявитель

{selectedRepair.requesterName || '—'}

Исполнитель

{selectedRepair.assignedTo || '—'}

Создана

{selectedRepair.createdAt ? new Date(selectedRepair.createdAt).toLocaleDateString('ru-RU') : '—'}

Ожидаемая дата возврата

{selectedRepair.expectedReturnDate ? new Date(selectedRepair.expectedReturnDate).toLocaleDateString('ru-RU') : '—'} {selectedRepair.expectedReturnDate && isOverdue(selectedRepair) && ( (просрочено) )}

{/* Данные по статусам: ожидание поставки, увезли на ремонт, договорились с подрядчиком */} {(selectedRepair.status === 'waiting_delivery' || selectedRepair.status === 'taken_for_repair' || selectedRepair.status === 'agreed_with_contractor') && (

Данные по текущему статусу

{selectedRepair.status === 'waiting_delivery' && ( <> {selectedRepair.waitingDeliveryDeadline && (

Примерный срок: {selectedRepair.waitingDeliveryDeadline}

)} {selectedRepair.waitingDeliveryContacts && (

Контакты для уточнения поставки: {selectedRepair.waitingDeliveryContacts}

)} )} {selectedRepair.status === 'taken_for_repair' && ( <> {selectedRepair.takenForRepairDeadline && (

Срок: {selectedRepair.takenForRepairDeadline}

)} {selectedRepair.takenForRepairContacts && (

Контакты: {selectedRepair.takenForRepairContacts}

)} )} {selectedRepair.status === 'agreed_with_contractor' && selectedRepair.agreedContractorPrice != null && selectedRepair.agreedContractorPrice > 0 && (

Цена (если известна): {selectedRepair.agreedContractorPrice} ₽

)}
)} {selectedRepair.startedAt && (

Взята в работу

{new Date(selectedRepair.startedAt).toLocaleDateString('ru-RU')}

)} {selectedRepair.completedAt && (

Выполнена

{new Date(selectedRepair.completedAt).toLocaleDateString('ru-RU')}

)} {selectedRepair.solution && (

Решение / что сделано

{selectedRepair.solution}

)} {(selectedRepair.isPaid && (selectedRepair.cost || 0) > 0) && (

Платный ремонт

{selectedRepair.cost} ₽{selectedRepair.costEstimated ? ' (предварительно)' : ''}

{selectedRepair.invoiceId && ( Счёт #{selectedRepair.invoiceId} )}
)}

Комментарии

{(selectedRepair.comments && selectedRepair.comments.length > 0) ? (
    {selectedRepair.comments.map((c, i) => (
  • {c.author} {c.createdAt && ( {new Date(c.createdAt).toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' })} )}

    {c.text}

  • ))}
) : (

Пока нет комментариев

)}