Files
mkd/components/finance/PaymentInvoiceDetail.tsx
2026-02-04 00:17:04 +05:00

730 lines
32 KiB
TypeScript
Executable File
Raw Permalink 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.
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>
);
};