import React, { useState, useEffect } from 'react'; import { X, Star, ChevronDown, Calendar, MapPin, User, Phone, FileText, HardHat, Users, History, MessageSquare, Paperclip, Send, Edit3, Printer, } from 'lucide-react'; import { DomaApplication, DomaApplicationStatus, ApplicationComment, ApplicationHistoryEntry } from '../../types'; import { backendApi } from '../../services/apiClient'; import { EditApplicationModal } from './EditApplicationModal'; import { StatusChangeModal } from './StatusChangeModal'; const STATUS_LABELS: Record = { new: 'Новая', in_progress: 'В работе', deferred: 'Отложена', done: 'Выполнена', canceled: 'Отменена', }; const SOURCE_LABELS: Record = { call: 'Звонок', website: 'Заявка с сайта', reception: 'Личный приём', mobile: 'Мобильное приложение', other: 'Другое', }; const STATUS_STYLE: Record = { new: { bg: 'bg-red-100', text: 'text-red-700' }, in_progress: { bg: 'bg-blue-100', text: 'text-blue-700' }, deferred: { bg: 'bg-orange-100', text: 'text-orange-700' }, done: { bg: 'bg-emerald-100', text: 'text-emerald-700' }, canceled: { bg: 'bg-slate-200', text: 'text-slate-600' }, }; const MAX_COMMENT_LENGTH = 1000; interface Props { applicationId: number; onClose: () => void; onUpdated?: () => void; } export const ApplicationCardDetail: React.FC = ({ applicationId, onClose, onUpdated }) => { const [application, setApplication] = useState(null); const [history, setHistory] = useState([]); const [comments, setComments] = useState([]); const [commentTab, setCommentTab] = useState<'internal' | 'resident'>('internal'); const [newComment, setNewComment] = useState(''); const [loading, setLoading] = useState(true); const [statusDropdownOpen, setStatusDropdownOpen] = useState(false); const [sendingComment, setSendingComment] = useState(false); const [editOpen, setEditOpen] = useState(false); const [statusModalTarget, setStatusModalTarget] = useState(null); const loadApplication = () => { backendApi.getApplication(applicationId).then(setApplication).catch(() => setApplication(null)); }; const loadHistory = () => { backendApi.getApplicationHistory(applicationId).then(setHistory).catch(() => setHistory([])); }; const loadComments = () => { backendApi.getApplicationComments(applicationId).then(setComments).catch(() => setComments([])); }; useEffect(() => { setLoading(true); loadApplication(); loadHistory(); loadComments(); setLoading(false); }, [applicationId]); const handleQuickStatusChange = async (newStatus: DomaApplicationStatus) => { if (!application) return; if (newStatus === 'canceled' || newStatus === 'deferred') { setStatusDropdownOpen(false); setStatusModalTarget(newStatus); return; } try { await backendApi.updateApplication(applicationId, { status: newStatus, changedBy: 'Администратор' }); window.dispatchEvent(new CustomEvent('mkd-applications-changed')); loadApplication(); loadHistory(); onUpdated?.(); setStatusDropdownOpen(false); } catch (err) { console.error(err); } }; const handleStatusModalConfirm = async (reason: string, deferredUntil?: string) => { if (!statusModalTarget || !application) return; try { await backendApi.updateApplication(applicationId, { status: statusModalTarget, statusReason: reason, deadlineAt: deferredUntil ? new Date(deferredUntil + 'T12:00:00').toISOString() : undefined, changedBy: 'Администратор', }); window.dispatchEvent(new CustomEvent('mkd-applications-changed')); loadApplication(); loadHistory(); onUpdated?.(); setStatusModalTarget(null); } catch (err) { throw err; } }; const handleSendComment = async () => { const text = newComment.trim(); if (!text || sendingComment) return; setSendingComment(true); try { await backendApi.addApplicationComment(applicationId, { text, type: commentTab, authorName: 'Администратор', }); setNewComment(''); loadComments(); } catch (err) { console.error(err); } finally { setSendingComment(false); } }; const formatDate = (s: string) => { const d = new Date(s); return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); }; const formatHistoryField = (fieldName: string) => { const map: Record = { status: 'Статус', deadline_at: 'Срок заявки', executor_name: 'Исполнитель', responsible_name: 'Ответственный', }; return map[fieldName] || fieldName; }; const formatHistoryEntry = (entry: ApplicationHistoryEntry) => { if (entry.fieldName === 'status_change' && entry.newValue) { return `${entry.changedBy}: ${entry.newValue}`; } if (entry.fieldName === 'deadline_at' && entry.oldValue && entry.newValue) { try { const oldD = new Date(entry.oldValue).toLocaleDateString('ru-RU'); const newD = new Date(entry.newValue).toLocaleDateString('ru-RU'); return `${entry.changedBy} изменил срок заявки с ${oldD} на ${newD}`; } catch { return `${entry.changedBy} изменил ${formatHistoryField(entry.fieldName)}`; } } return `${entry.changedBy} изменил ${formatHistoryField(entry.fieldName)}`; }; if (loading || !application) { return (
{loading ? 'Загрузка…' : 'Заявка не найдена'}
); } const statusLabel = STATUS_LABELS[application.status] || application.status; const statusStyle = STATUS_STYLE[application.status]; const isOverdue = application.isOverdue || (application.status !== 'done' && application.status !== 'canceled' && new Date(application.deadlineAt) < new Date()); const sourceLabel = application.sourceChannel ? SOURCE_LABELS[application.sourceChannel] || application.sourceChannel : 'Не указан'; const handlePrint = () => window.print(); return (
{/* Header */}

Заявка № {application.number}

Дата создания {formatDate(application.createdAt)}, автор Администратор (вы)

Источник — {sourceLabel}

{statusDropdownOpen && ( <>
setStatusDropdownOpen(false)} />
{(Object.keys(STATUS_LABELS) as DomaApplicationStatus[]).map((s) => ( ))}
)}
{/* Icons (muted call / doc) */}
{/* Fields */}

{formatDate(application.deadlineAt)} {isOverdue && (просрочена на день)}

{application.address} {application.apartment && ` квартира ${application.apartment}`}

{application.contactName || application.clientName || 'Не указан'} {application.contactPhone && ( {application.contactPhone} )}

{application.description}

{application.placeIncident || '—'} {application.workType && ` » ${application.workType}`}

{application.executorName || application.performer?.name || 'Сотрудник не указан или был удален'}

{application.responsibleName || 'Не указан'}

{application.observersText || 'Сотрудники не указаны или были удалены'}

{/* История изменений заявки */}

История изменений заявки

    {history.length === 0 ? (
  • Нет записей
  • ) : ( history.map((entry) => (
  • {formatDate(entry.changedAt)}: {formatHistoryEntry(entry)}
  • )) )}
{/* Комментарии: Внутренние / С жителем */}

Комментарии

{comments.filter((c) => c.type === commentTab).length === 0 ? (

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

) : ( comments .filter((c) => c.type === commentTab) .map((c) => (

{c.authorName} (сотрудник) · {formatDate(c.createdAt)}

{c.text}

)) )}
setNewComment(e.target.value.slice(0, MAX_COMMENT_LENGTH))} placeholder="Ваш комментарий" className="flex-1 px-4 py-2.5 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500" /> {newComment.length}/{MAX_COMMENT_LENGTH}
{/* Действия */}
{editOpen && application && ( setEditOpen(false)} application={application} onSuccess={() => { loadApplication(); loadHistory(); onUpdated?.(); }} /> )} {statusModalTarget && application && (statusModalTarget === 'canceled' || statusModalTarget === 'deferred') && ( setStatusModalTarget(null)} application={{ id: application.id, number: application.number, address: application.address, currentStatus: application.status }} newStatus={statusModalTarget} onConfirm={handleStatusModalConfirm} /> )}
); };