Files
mkd/components/finance/PaymentInvoiceDetail.tsx

730 lines
32 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
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<PaymentInvoice['status'], { label: string; color: string; bg: string }> = {
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<PaymentInvoice['purposeType'], any> = {
building: Building2,
district: MapPin,
legal: Briefcase,
office: FileText,
hr: Users,
event: PartyPopper,
other: FileText
};
export const PaymentInvoiceDetail: React.FC<PaymentInvoiceDetailProps> = ({
invoice,
currentUserId,
onBack,
onUpdate
}) => {
const [userRoles, setUserRoles] = useState<string[]>([]);
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<HTMLInputElement | null>(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<UserRole[]>(`/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<string, { role?: string }> = {
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<HTMLInputElement>) => {
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 (
<div className="space-y-6 animate-fade-in">
{/* Заголовок */}
<div className="flex items-center justify-between">
<button
onClick={onBack}
className="flex items-center gap-2 text-primary-600 hover:text-primary-700 font-medium"
>
<ArrowLeft className="w-4 h-4" />
Назад к списку
</button>
<div className={`px-3 py-1 rounded-lg ${statusInfo.bg} ${statusInfo.color}`}>
<span className="text-sm font-bold">{statusInfo.label}</span>
</div>
</div>
{/* Основная информация */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<div className="flex items-start justify-between mb-6">
<div>
<h2 className="text-xl font-bold text-slate-800 mb-1">Счет {invoice.invoiceNumber}</h2>
<p className="text-sm text-slate-500">
Создан {new Date(invoice.createdAt).toLocaleDateString('ru-RU')} пользователем {invoice.createdBy}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Назначение */}
<div>
<h3 className="text-sm font-bold text-slate-700 mb-2 flex items-center gap-2">
<PurposeIcon className="w-4 h-4" />
Назначение
</h3>
<p className="text-slate-800">
{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}
</p>
</div>
{/* Формат оплаты */}
<div>
<h3 className="text-sm font-bold text-slate-700 mb-2 flex items-center gap-2">
<Calculator className="w-4 h-4" />
Формат оплаты
</h3>
<p className="text-slate-800">
{invoice.paymentFormat === 'prepayment' && 'Предоплата'}
{invoice.paymentFormat === 'postpayment' && 'Постоплата'}
{invoice.paymentFormat === 'advance' && 'Аванс'}
</p>
</div>
{/* Подрядчик */}
<div>
<h3 className="text-sm font-bold text-slate-700 mb-2">Подрядчик</h3>
<p className="text-slate-800 font-medium">{invoice.contractorName}</p>
{invoice.contractorInn && (
<p className="text-sm text-slate-500">ИНН: {invoice.contractorInn}</p>
)}
</div>
{/* Сумма */}
<div>
<h3 className="text-sm font-bold text-slate-700 mb-2">Сумма</h3>
<p className="text-2xl font-black text-slate-900">
{invoice.totalAmount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</p>
</div>
{/* Услуги или ТМЦ */}
<div className="md:col-span-2">
<h3 className="text-sm font-bold text-slate-700 mb-2">
{invoice.itemType === 'service' ? 'Услуги' : 'ТМЦ (товарно-материальные ценности)'}
</h3>
{invoice.itemType === 'service' && invoice.serviceItems && invoice.serviceItems.length > 0 ? (
<div className="space-y-2">
{invoice.serviceItems.map((item, index) => (
<div key={index} className="flex justify-between items-center p-2 bg-slate-50 rounded-lg">
<span className="text-slate-800 font-medium">{item.name}</span>
<span className="text-slate-900 font-bold">
{item.amount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
</div>
))}
</div>
) : invoice.itemType === 'materials' && invoice.materialItems && invoice.materialItems.length > 0 ? (
<div className="space-y-2">
{invoice.materialItems.map((item, index) => (
<div key={index} className="p-2 bg-slate-50 rounded-lg">
<div className="flex justify-between items-center mb-1">
<span className="text-slate-800 font-medium">{item.name}</span>
<span className="text-slate-900 font-bold">
{item.amount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
</div>
<div className="text-xs text-slate-500">
{item.quantity} {item.unit} × {item.pricePerUnit.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
</div>
))}
</div>
) : (
<p className="text-slate-800">{invoice.serviceDescription || 'Нет данных'}</p>
)}
</div>
</div>
{/* Даты */}
<div className="mt-6 pt-6 border-t border-slate-200 grid grid-cols-1 md:grid-cols-3 gap-4">
{invoice.scheduledDate && (
<div>
<p className="text-xs text-slate-500 mb-1">Дата в графике</p>
<p className="text-sm font-medium text-slate-800">
{new Date(invoice.scheduledDate).toLocaleDateString('ru-RU')}
</p>
</div>
)}
{invoice.paymentDate && (
<div>
<p className="text-xs text-slate-500 mb-1">Дата оплаты</p>
<p className="text-sm font-medium text-slate-800">
{new Date(invoice.paymentDate).toLocaleDateString('ru-RU')}
</p>
</div>
)}
{invoice.status === 'paid' && invoice.paymentRef && (
<div>
<p className="text-xs text-slate-500 mb-1">Номер платежки</p>
<p className="text-sm font-medium text-slate-800">{invoice.paymentRef}</p>
</div>
)}
{invoice.status === 'paid' && invoice.isCash && (
<div>
<p className="text-xs text-slate-500 mb-1">Способ оплаты</p>
<p className="text-sm font-medium text-amber-700 flex items-center gap-1">
<Banknote className="w-4 h-4" /> Оплата наличными
</p>
</div>
)}
{invoice.isCompleted && (
<div>
<p className="text-xs text-slate-500 mb-1">Статус выполнения</p>
<p className="text-sm font-medium text-emerald-600 flex items-center gap-1">
<CheckCircle2 className="w-4 h-4" />
Выполнено
</p>
</div>
)}
{(invoice.paymentFormat === 'prepayment' || invoice.paymentFormat === 'advance') && (
<div>
<p className="text-xs text-slate-500 mb-1">Закрывающие документы</p>
<p className="text-sm font-medium text-slate-800">
{invoice.closingDocsReceived ? 'Получены' : 'Не получены'}
</p>
</div>
)}
</div>
{/* Загруженные файлы (физические счета) */}
{invoice.fileUrls && invoice.fileUrls.length > 0 && (
<div className="mt-6 pt-6 border-t border-slate-200">
<h3 className="text-sm font-bold text-slate-700 mb-4 flex items-center gap-2">
<Paperclip className="w-4 h-4" />
Физические счета
</h3>
<div className="space-y-2">
{invoice.fileUrls.map((fileUrl, index) => {
// fileUrl может быть строкой или объектом с информацией о файле
const fileInfo = typeof fileUrl === 'string'
? { url: fileUrl, filename: fileUrl.split('/').pop() || 'Файл' }
: fileUrl;
return (
<div key={index} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg border border-slate-200">
<div className="flex items-center gap-2 flex-1 min-w-0">
<FileText className="w-4 h-4 text-slate-500 flex-shrink-0" />
<span className="text-sm text-slate-700 truncate" title={fileInfo.filename || fileInfo.url}>
{fileInfo.filename || fileInfo.url}
</span>
{fileInfo.size && (
<span className="text-xs text-slate-500 flex-shrink-0">
({(fileInfo.size / 1024).toFixed(1)} KB)
</span>
)}
</div>
<a
href={fileInfo.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 px-3 py-1.5 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm font-medium"
>
<Download className="w-4 h-4" />
Скачать
</a>
</div>
);
})}
</div>
</div>
)}
{/* Файлы закрывающих документов (сканы/фото) */}
{invoice.closingDocsFiles && invoice.closingDocsFiles.length > 0 && (
<div className="mt-6 pt-6 border-t border-slate-200">
<h3 className="text-sm font-bold text-slate-700 mb-4 flex items-center gap-2">
<Paperclip className="w-4 h-4" />
Закрывающие документы
</h3>
<div className="space-y-2">
{invoice.closingDocsFiles.map((file: any, index: number) => {
const fileInfo = file || {};
return (
<div key={index} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg border border-slate-200">
<div className="flex items-center gap-2 flex-1 min-w-0">
<FileText className="w-4 h-4 text-slate-500 flex-shrink-0" />
<span className="text-sm text-slate-700 truncate" title={fileInfo.filename || fileInfo.url}>
{fileInfo.filename || fileInfo.url || 'Файл'}
</span>
{fileInfo.size && (
<span className="text-xs text-slate-500 flex-shrink-0">
({(fileInfo.size / 1024).toFixed(1)} KB)
</span>
)}
</div>
{fileInfo.url && (
<a
href={fileInfo.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 px-3 py-1.5 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm font-medium"
>
<Download className="w-4 h-4" />
Скачать
</a>
)}
</div>
);
})}
</div>
</div>
)}
{/* Примечания */}
{invoice.notes && (
<div className="mt-6 pt-6 border-t border-slate-200">
<h3 className="text-sm font-bold text-slate-700 mb-2">Примечания</h3>
<p className="text-slate-800">{invoice.notes}</p>
</div>
)}
{/* История согласований */}
{invoice.approvalHistory && invoice.approvalHistory.length > 0 && (
<div className="mt-6 pt-6 border-t border-slate-200">
<h3 className="text-sm font-bold text-slate-700 mb-4 flex items-center gap-2">
<History className="w-4 h-4" />
История согласований
</h3>
<div className="space-y-3">
{invoice.approvalHistory.map((entry, index) => (
<div key={index} className="flex items-start gap-3 p-3 bg-slate-50 rounded-lg">
<div className={`p-1.5 rounded ${
entry.action === 'approve' ? 'bg-emerald-100 text-emerald-600' :
entry.action === 'reject' ? 'bg-red-100 text-red-600' :
'bg-amber-100 text-amber-600'
}`}>
{entry.action === 'approve' ? (
<Check className="w-4 h-4" />
) : entry.action === 'reject' ? (
<X className="w-4 h-4" />
) : (
<Clock className="w-4 h-4" />
)}
</div>
<div className="flex-1">
<p className="text-sm font-medium text-slate-800">
{entry.action === 'approve' ? 'Согласовано' :
entry.action === 'reject' ? 'Отклонено' : 'На доработку'}
</p>
<p className="text-xs text-slate-500">
{entry.userRole} {new Date(entry.date).toLocaleString('ru-RU')}
</p>
{entry.comment && (
<p className="text-sm text-slate-700 mt-1">{entry.comment}</p>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* Причина отклонения */}
{invoice.status === 'rejected' && invoice.rejectionReason && (
<div className="mt-6 pt-6 border-t border-slate-200">
<h3 className="text-sm font-bold text-red-700 mb-2">Причина отклонения</h3>
<p className="text-red-600">{invoice.rejectionReason}</p>
</div>
)}
{/* Причина отмены */}
{invoice.status === 'cancelled' && invoice.cancelReason && (
<div className="mt-6 pt-6 border-t border-slate-200">
<h3 className="text-sm font-bold text-slate-700 mb-2">Причина отмены</h3>
<p className="text-slate-600">{invoice.cancelReason}</p>
</div>
)}
</div>
{/* Действия */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<h3 className="text-sm font-bold text-slate-700 mb-4">Действия</h3>
{canApprove() && (
<div className="space-y-4 mb-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Комментарий (необязательно)</label>
<textarea
value={approvalComment}
onChange={(e) => setApprovalComment(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
rows={2}
placeholder="Добавьте комментарий..."
/>
</div>
<div className="flex gap-3">
<button
onClick={handleApprove}
disabled={loading}
className="flex-1 px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 disabled:bg-slate-300 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
<Check className="w-4 h-4" />
Согласовать
</button>
<button
onClick={() => {
const reason = prompt('Укажите причину отклонения:');
if (reason) {
setRejectionReason(reason);
setTimeout(() => handleReject(), 100);
}
}}
disabled={loading}
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:bg-slate-300 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
<X className="w-4 h-4" />
Отклонить
</button>
</div>
</div>
)}
{canSchedule() && (
<div className="space-y-4 mb-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Дата в графике платежей *</label>
<input
type="date"
value={scheduledDate}
onChange={(e) => setScheduledDate(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
/>
</div>
<button
onClick={handleSchedule}
disabled={loading || !scheduledDate}
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:bg-slate-300 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
<Calendar className="w-4 h-4" />
Поставить в график платежей
</button>
</div>
)}
{invoice.status === 'scheduled' && (
<div className="space-y-2 mb-4">
<button
onClick={() => setShowPayModal('default')}
disabled={loading}
className="w-full px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 disabled:bg-slate-300 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
<Wallet className="w-4 h-4" />
Оплатить
</button>
<button
onClick={() => setShowPayModal('cash')}
disabled={loading}
className="w-full px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 disabled:bg-slate-300 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
<Banknote className="w-4 h-4" />
Оплата наличкой
</button>
</div>
)}
{canMarkCompleted() && (
<button
onClick={handleMarkCompleted}
disabled={loading}
className="w-full px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-slate-300 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
<CheckCircle2 className="w-4 h-4" />
{invoice.itemType === 'materials' ? 'Отметить полученным (ТМЦ)' : 'Отметить выполненным'}
</button>
)}
{(invoice.paymentFormat === 'prepayment' || invoice.paymentFormat === 'advance') &&
invoice.isCompleted && (
<div className="mt-4 space-y-3">
{!invoice.closingDocsReceived && (
<button
onClick={handleClosingDocsReceived}
disabled={closingDocsUpdating}
className="w-full px-4 py-2 bg-sky-600 text-white rounded-lg hover:bg-sky-700 disabled:bg-slate-300 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
<Check className="w-4 h-4" />
Подтвердить получение закрывающих документов
</button>
)}
<div className="flex flex-col sm:flex-row gap-2 items-stretch sm:items-center">
<input
ref={closingDocsFileInputRef}
type="file"
className="hidden"
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.gif,.webp,.txt,.zip,.rar"
onChange={handleClosingDocsFileChange}
/>
<button
type="button"
onClick={() => closingDocsFileInputRef.current?.click()}
disabled={closingDocsUploading}
className="flex-1 px-4 py-2 border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 disabled:bg-slate-100 disabled:text-slate-400 flex items-center justify-center gap-2 text-sm"
>
<Paperclip className="w-4 h-4" />
{closingDocsUploading ? 'Загрузка файла...' : 'Прикрепить скан/фото закрывающих'}
</button>
</div>
</div>
)}
</div>
{showPayModal !== null && (
<PaymentStatusModal
action="paid"
invoiceLabel={`${invoice.contractorName}${invoice.totalAmount.toLocaleString('ru-RU')}`}
defaultIsCash={showPayModal === 'cash'}
onConfirm={handleMarkPaid}
onCancel={() => setShowPayModal(null)}
/>
)}
</div>
);
};