import React, { useState, useEffect, useRef } from 'react'; import { PaymentInvoice, UserRole } from '../../types'; import { apiClient } from '../../services/apiClient'; import { ArrowLeft, Check, X, Calendar, Clock, FileText, Building2, MapPin, Briefcase, Users, Calculator, History, CheckCircle2, Paperclip, Download, PartyPopper, Wallet, Banknote } from 'lucide-react'; import { PaymentStatusModal, PaymentStatusModalPayload } from './PaymentStatusModal'; interface PaymentInvoiceDetailProps { invoice: PaymentInvoice; currentUserId: string; onBack: () => void; onUpdate: () => void; } const StatusConfig: Record = { draft: { label: 'Черновик', color: 'text-slate-500', bg: 'bg-slate-100' }, pending_manager_approval: { label: 'На согласовании у руководителя', color: 'text-amber-600', bg: 'bg-amber-50' }, pending_finance_manager_approval: { label: 'На согласовании у финансового руководителя', color: 'text-blue-600', bg: 'bg-blue-50' }, approved: { label: 'Согласован', color: 'text-indigo-600', bg: 'bg-indigo-50' }, scheduled: { label: 'В графике платежей', color: 'text-purple-600', bg: 'bg-purple-50' }, paid: { label: 'Оплачен', color: 'text-emerald-600', bg: 'bg-emerald-50' }, postponed: { label: 'Отложен', color: 'text-orange-600', bg: 'bg-orange-50' }, cancelled: { label: 'Отменен', color: 'text-red-600', bg: 'bg-red-50' }, rejected: { label: 'Отклонен', color: 'text-red-600', bg: 'bg-red-50' }, completed: { label: 'Выполнено', color: 'text-green-600', bg: 'bg-green-50' } }; const PurposeTypeIcons: Record = { building: Building2, district: MapPin, legal: Briefcase, office: FileText, hr: Users, event: PartyPopper, other: FileText }; export const PaymentInvoiceDetail: React.FC = ({ invoice, currentUserId, onBack, onUpdate }) => { const [userRoles, setUserRoles] = useState([]); const [loading, setLoading] = useState(false); const [rejectionReason, setRejectionReason] = useState(''); const [approvalComment, setApprovalComment] = useState(''); const [scheduledDate, setScheduledDate] = useState(invoice.scheduledDate || ''); const [closingDocsUpdating, setClosingDocsUpdating] = useState(false); const closingDocsFileInputRef = useRef(null); const [closingDocsUploading, setClosingDocsUploading] = useState(false); const [showPayModal, setShowPayModal] = useState<'default' | 'cash' | null>(null); useEffect(() => { fetchUserRoles(); }, [currentUserId]); const fetchUserRoles = async () => { try { const roles = await apiClient.get(`/finance/user-roles?userId=${currentUserId}`); setUserRoles(roles.map(r => r.role)); } catch (err) { console.error('Error fetching user roles:', err); } }; const canApprove = () => { // Проверка прав на согласование const TOP_MANAGEMENT_ROLES = ['finance_director', 'director', 'top_management']; const statusConfig: Record = { pending_manager_approval: { role: 'manager' }, pending_finance_manager_approval: { role: 'finance_manager' } }; const config = statusConfig[invoice.status]; if (!config || !config.role) return false; // Если требуется роль manager, но пользователь из высшего звена - может согласовать if (config.role === 'manager' && userRoles.some(role => TOP_MANAGEMENT_ROLES.includes(role))) { return true; } return userRoles.includes(config.role); }; const canSchedule = () => { // Ставить в график может финансист ИЛИ финансовый руководитель. // В демо, если ролей нет, считаем текущего пользователя финансовым блоком. const canScheduleRole = userRoles.includes('financier') || userRoles.includes('finance_manager') || userRoles.length === 0; if (!canScheduleRole) return false; // Для постоплаты можем поставить в график после согласования или после выполнения if (invoice.paymentFormat === 'postpayment') { return ['approved', 'completed'].includes(invoice.status); } // Для предоплаты и аванса ставим в график первый платеж после согласования if (invoice.paymentFormat === 'prepayment' || invoice.paymentFormat === 'advance') { return invoice.status === 'approved'; } return invoice.status === 'approved'; }; const canMarkCompleted = () => { if (invoice.paymentFormat === 'postpayment') { return ['approved', 'scheduled'].includes(invoice.status) && !invoice.isCompleted; } else if (invoice.paymentFormat === 'prepayment' || invoice.paymentFormat === 'advance') { return invoice.status === 'paid' && !invoice.isCompleted; } return false; }; const handleApprove = async () => { try { setLoading(true); await apiClient.post(`/finance/payment-invoices/${invoice.id}/approve`, { userId: currentUserId, comment: approvalComment }); setApprovalComment(''); onUpdate(); } catch (err) { console.error('Error approving invoice:', err); alert('Ошибка при согласовании счета'); } finally { setLoading(false); } }; const handleReject = async () => { if (!rejectionReason.trim()) { alert('Укажите причину отклонения'); return; } try { setLoading(true); await apiClient.post(`/finance/payment-invoices/${invoice.id}/reject`, { userId: currentUserId, reason: rejectionReason }); setRejectionReason(''); onUpdate(); } catch (err) { console.error('Error rejecting invoice:', err); alert('Ошибка при отклонении счета'); } finally { setLoading(false); } }; const handleSchedule = async () => { if (!scheduledDate) { alert('Укажите дату для графика платежей'); return; } try { setLoading(true); await apiClient.post(`/finance/payment-invoices/${invoice.id}/schedule`, { userId: currentUserId, scheduledDate }); onUpdate(); } catch (err) { console.error('Error scheduling invoice:', err); alert('Ошибка при постановке в график'); } finally { setLoading(false); } }; const handleMarkCompleted = async () => { try { setLoading(true); await apiClient.post(`/finance/payment-invoices/${invoice.id}/mark-completed`); onUpdate(); } catch (err) { console.error('Error marking as completed:', err); alert('Ошибка при отметке выполнения'); } finally { setLoading(false); } }; const handleClosingDocsReceived = async () => { try { setClosingDocsUpdating(true); await apiClient.post(`/finance/payment-invoices/${invoice.id}/closing-docs`, { received: true }); onUpdate(); } catch (err) { console.error('Error updating closing docs flag:', err); alert('Ошибка при отметке закрывающих документов'); } finally { setClosingDocsUpdating(false); } }; const handleMarkPaid = async (payload: PaymentStatusModalPayload) => { try { setLoading(true); await apiClient.post(`/finance/payment-invoices/${invoice.id}/update-payment-status`, { status: 'paid', paymentDate: payload.paymentDate, paymentRef: payload.paymentRef, isCash: payload.isCash }); setShowPayModal(null); onUpdate(); } catch (err) { console.error('Error marking invoice as paid:', err); alert('Ошибка при отметке оплаты'); } finally { setLoading(false); } }; const handleClosingDocsFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; try { setClosingDocsUploading(true); // Загружаем файл на сервер (как для обычных счетов) const formData = new FormData(); formData.append('file', file); const uploadResponse = await fetch('/api/finance/payment-invoices/upload', { method: 'POST', body: formData }); if (!uploadResponse.ok) { throw new Error('Ошибка загрузки файла'); } const data = await uploadResponse.json(); const fileInfo = data.file; // Привязываем файл к счету как закрывающий документ и помечаем, что закрывашки получены await apiClient.post(`/finance/payment-invoices/${invoice.id}/closing-docs`, { received: true, files: [fileInfo] }); onUpdate(); } catch (err) { console.error('Error uploading closing docs file:', err); alert('Ошибка при загрузке файла закрывающих документов'); } finally { setClosingDocsUploading(false); if (closingDocsFileInputRef.current) { closingDocsFileInputRef.current.value = ''; } } }; const PurposeIcon = PurposeTypeIcons[invoice.purposeType] || FileText; const statusInfo = StatusConfig[invoice.status]; return (
{/* Заголовок */}
{statusInfo.label}
{/* Основная информация */}

Счет № {invoice.invoiceNumber}

Создан {new Date(invoice.createdAt).toLocaleDateString('ru-RU')} пользователем {invoice.createdBy}

{/* Назначение */}

Назначение

{invoice.purposeType === 'building' && invoice.purposeBuildingIds.length > 0 && ( <>Дом ({invoice.purposeBuildingIds.length} шт.) )} {invoice.purposeType === 'district' && invoice.purposeDistrictIds.length > 0 && ( <>Участок ({invoice.purposeDistrictIds.length} шт.) )} {['legal', 'office', 'hr'].includes(invoice.purposeType) && ( <>{invoice.purposeType === 'legal' ? 'Юристы' : invoice.purposeType === 'office' ? 'Офис' : 'HR'} )} {invoice.purposeType === 'event' && (invoice.purposeDescription || 'Мероприятие')} {invoice.purposeType === 'other' && invoice.purposeDescription}

{/* Формат оплаты */}

Формат оплаты

{invoice.paymentFormat === 'prepayment' && 'Предоплата'} {invoice.paymentFormat === 'postpayment' && 'Постоплата'} {invoice.paymentFormat === 'advance' && 'Аванс'}

{/* Подрядчик */}

Подрядчик

{invoice.contractorName}

{invoice.contractorInn && (

ИНН: {invoice.contractorInn}

)}
{/* Сумма */}

Сумма

{invoice.totalAmount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽

{/* Услуги или ТМЦ */}

{invoice.itemType === 'service' ? 'Услуги' : 'ТМЦ (товарно-материальные ценности)'}

{invoice.itemType === 'service' && invoice.serviceItems && invoice.serviceItems.length > 0 ? (
{invoice.serviceItems.map((item, index) => (
{item.name} {item.amount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽
))}
) : invoice.itemType === 'materials' && invoice.materialItems && invoice.materialItems.length > 0 ? (
{invoice.materialItems.map((item, index) => (
{item.name} {item.amount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽
{item.quantity} {item.unit} × {item.pricePerUnit.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽
))}
) : (

{invoice.serviceDescription || 'Нет данных'}

)}
{/* Даты */}
{invoice.scheduledDate && (

Дата в графике

{new Date(invoice.scheduledDate).toLocaleDateString('ru-RU')}

)} {invoice.paymentDate && (

Дата оплаты

{new Date(invoice.paymentDate).toLocaleDateString('ru-RU')}

)} {invoice.status === 'paid' && invoice.paymentRef && (

Номер платежки

{invoice.paymentRef}

)} {invoice.status === 'paid' && invoice.isCash && (

Способ оплаты

Оплата наличными

)} {invoice.isCompleted && (

Статус выполнения

Выполнено

)} {(invoice.paymentFormat === 'prepayment' || invoice.paymentFormat === 'advance') && (

Закрывающие документы

{invoice.closingDocsReceived ? 'Получены' : 'Не получены'}

)}
{/* Загруженные файлы (физические счета) */} {invoice.fileUrls && invoice.fileUrls.length > 0 && (

Физические счета

{invoice.fileUrls.map((fileUrl, index) => { // fileUrl может быть строкой или объектом с информацией о файле const fileInfo = typeof fileUrl === 'string' ? { url: fileUrl, filename: fileUrl.split('/').pop() || 'Файл' } : fileUrl; return (
{fileInfo.filename || fileInfo.url} {fileInfo.size && ( ({(fileInfo.size / 1024).toFixed(1)} KB) )}
Скачать
); })}
)} {/* Файлы закрывающих документов (сканы/фото) */} {invoice.closingDocsFiles && invoice.closingDocsFiles.length > 0 && (

Закрывающие документы

{invoice.closingDocsFiles.map((file: any, index: number) => { const fileInfo = file || {}; return (
{fileInfo.filename || fileInfo.url || 'Файл'} {fileInfo.size && ( ({(fileInfo.size / 1024).toFixed(1)} KB) )}
{fileInfo.url && ( Скачать )}
); })}
)} {/* Примечания */} {invoice.notes && (

Примечания

{invoice.notes}

)} {/* История согласований */} {invoice.approvalHistory && invoice.approvalHistory.length > 0 && (

История согласований

{invoice.approvalHistory.map((entry, index) => (
{entry.action === 'approve' ? ( ) : entry.action === 'reject' ? ( ) : ( )}

{entry.action === 'approve' ? 'Согласовано' : entry.action === 'reject' ? 'Отклонено' : 'На доработку'}

{entry.userRole} • {new Date(entry.date).toLocaleString('ru-RU')}

{entry.comment && (

{entry.comment}

)}
))}
)} {/* Причина отклонения */} {invoice.status === 'rejected' && invoice.rejectionReason && (

Причина отклонения

{invoice.rejectionReason}

)} {/* Причина отмены */} {invoice.status === 'cancelled' && invoice.cancelReason && (

Причина отмены

{invoice.cancelReason}

)}
{/* Действия */}

Действия

{canApprove() && (