import React, { useState, useEffect } from 'react'; import { PaymentInvoice, PaymentInvoicePurposeType, PaymentInvoiceFormat, PaymentInvoiceItemType, Building, District, ServiceItem, MaterialItem, PlanItem, DistributionMethod } from '../../types'; import { backendApi } from '../../services/apiClient'; import { apiClient } from '../../services/apiClient'; import { X, Save, Upload, Building2, MapPin, Briefcase, Users, FileText, Calculator, Plus, Trash2, Paperclip, Download, PartyPopper, CalendarDays } from 'lucide-react'; import { InvoiceDistribution } from './InvoiceDistribution'; export type PaymentInvoiceInitialPrefill = | { purposeType: 'event'; purposeEventId?: string; purposeDescription?: string; totalAmount?: number; } | { purposeType: 'office'; purposeDescription?: string; contractorName?: string; totalAmount?: number; itemType?: 'service' | 'materials'; serviceItems?: { name: string; amount: number }[]; materialItems?: { name: string; quantity: number; unit: string; pricePerUnit: number; amount: number }[]; notes?: string; }; interface PaymentInvoiceFormProps { invoice?: PaymentInvoice; initialPrefill?: PaymentInvoiceInitialPrefill; currentUserId: string; onSave: (invoice: Partial) => Promise; onCancel: () => void; } export const PaymentInvoiceForm: React.FC = ({ invoice, initialPrefill, currentUserId, onSave, onCancel }) => { const [formData, setFormData] = useState(() => { const prefill = initialPrefill; const purposeType = (invoice?.purposeType || (prefill && 'purposeType' in prefill ? prefill.purposeType : undefined) || 'building') as PaymentInvoicePurposeType; const officePrefill = prefill && 'purposeType' in prefill && prefill.purposeType === 'office' ? prefill : null; return { purposeType, purposeBuildingIds: invoice?.purposeBuildingIds || [], purposeDistrictIds: invoice?.purposeDistrictIds || [], purposeDescription: invoice?.purposeDescription ?? (prefill && 'purposeDescription' in prefill ? prefill.purposeDescription : undefined) ?? '', purposeEventId: invoice?.purposeEventId ?? (prefill && 'purposeEventId' in prefill ? prefill.purposeEventId : undefined) ?? undefined as number | string | undefined, planItemId: invoice?.planItemId ?? undefined as string | undefined, planItemBuildingId: invoice?.planItemBuildingId ?? undefined as string | undefined, fromWorkPlan: !!(invoice?.planItemId && invoice?.planItemBuildingId), paymentFormat: (invoice?.paymentFormat || 'postpayment') as PaymentInvoiceFormat, itemType: (invoice?.itemType || (officePrefill?.itemType ?? 'service')) as PaymentInvoiceItemType, contractorName: invoice?.contractorName || (officePrefill?.contractorName ?? '') || '', contractorInn: invoice?.contractorInn || '', serviceDescription: invoice?.serviceDescription || '', serviceItems: invoice?.serviceItems ?? (officePrefill?.itemType !== 'materials' && (officePrefill?.serviceItems?.length ? officePrefill.serviceItems : officePrefill?.purposeDescription && (officePrefill?.totalAmount ?? 0) > 0 ? [{ name: officePrefill.purposeDescription!, amount: officePrefill.totalAmount! }] : undefined)) ?? (invoice?.itemType === 'service' ? [] : undefined), materialItems: invoice?.materialItems ?? (officePrefill?.itemType === 'materials' && (officePrefill?.materialItems?.length ? officePrefill.materialItems : officePrefill?.purposeDescription && (officePrefill?.totalAmount ?? 0) > 0 ? [{ name: officePrefill.purposeDescription, quantity: 1, unit: 'шт', pricePerUnit: officePrefill.totalAmount!, amount: officePrefill.totalAmount! }] : undefined)) ?? (invoice?.itemType === 'materials' ? [] : undefined), totalAmount: invoice?.totalAmount ?? (prefill && 'totalAmount' in prefill ? prefill.totalAmount : undefined) ?? 0, notes: invoice?.notes || (officePrefill?.notes ?? '') || '', fileUrls: invoice?.fileUrls || [], distributionMethod: (invoice?.distributionMethod ?? undefined) as DistributionMethod | undefined, distributionData: invoice?.distributionData ?? {} }; }); const [buildings, setBuildings] = useState([]); const [districts, setDistricts] = useState([]); const [loading, setLoading] = useState(false); const [errors, setErrors] = useState>({}); const [isUploadingFile, setIsUploadingFile] = useState(false); const fileInputRef = React.useRef(null); const currentYear = new Date().getFullYear(); const planSourceBuildings = React.useMemo(() => { if (formData.purposeType === 'building' && formData.purposeBuildingIds.length > 0) { return buildings.filter(b => formData.purposeBuildingIds.includes(b.id)); } if (formData.purposeType === 'district' && formData.purposeDistrictIds.length > 0) { return buildings.filter(b => formData.purposeDistrictIds.includes((b as any).districtId)); } return []; }, [formData.purposeType, formData.purposeBuildingIds, formData.purposeDistrictIds, buildings]); const planItemsForSelect = React.useMemo(() => { const items: { item: PlanItem; buildingId: string; buildingLabel: string }[] = []; for (const b of planSourceBuildings) { const plan = (b as any).annualPlan || []; const arr = Array.isArray(plan) ? plan : []; for (const p of arr) { if ((p.year || currentYear) === currentYear) { items.push({ item: p, buildingId: b.id, buildingLabel: (b.passport as any)?.address || b.id }); } } } return items; }, [planSourceBuildings, currentYear]); // Для распределения: при «Дом» — выбранные дома; при «Участок» — все дома на выбранном участке(ах) const distributionBuildingIds = React.useMemo(() => { if (formData.purposeType === 'building') return formData.purposeBuildingIds; if (formData.purposeType === 'district') return planSourceBuildings.map(b => b.id); return []; }, [formData.purposeType, formData.purposeBuildingIds, planSourceBuildings]); useEffect(() => { fetchData(); }, []); const fetchData = async () => { try { const [buildingsData, districtsData] = await Promise.all([ backendApi.getBuildings(), backendApi.getDistricts() ]); setBuildings(buildingsData); setDistricts(districtsData); } catch (err) { console.error('Error fetching data:', err); } }; const validate = (): boolean => { const newErrors: Record = {}; if (!formData.contractorName.trim()) { newErrors.contractorName = 'Название подрядчика обязательно'; } // Валидация в зависимости от типа if (formData.itemType === 'service') { if (!formData.serviceItems || formData.serviceItems.length === 0) { newErrors.serviceItems = 'Добавьте хотя бы одну услугу'; } else { // Проверяем, что все услуги заполнены formData.serviceItems.forEach((item, index) => { if (!item.name.trim()) { newErrors[`serviceItem_${index}_name`] = 'Название услуги обязательно'; } if (item.amount <= 0) { newErrors[`serviceItem_${index}_amount`] = 'Сумма должна быть больше нуля'; } }); } } else if (formData.itemType === 'materials') { if (!formData.materialItems || formData.materialItems.length === 0) { newErrors.materialItems = 'Добавьте хотя бы одну позицию ТМЦ'; } else { // Проверяем, что все ТМЦ заполнены formData.materialItems.forEach((item, index) => { if (!item.name.trim()) { newErrors[`materialItem_${index}_name`] = 'Наименование ТМЦ обязательно'; } if (item.quantity <= 0) { newErrors[`materialItem_${index}_quantity`] = 'Количество должно быть больше нуля'; } if (!item.unit.trim()) { newErrors[`materialItem_${index}_unit`] = 'Единица измерения обязательна'; } if (item.pricePerUnit <= 0) { newErrors[`materialItem_${index}_price`] = 'Цена за единицу должна быть больше нуля'; } }); } } if (formData.totalAmount <= 0) { newErrors.totalAmount = 'Общая сумма должна быть больше нуля'; } // Для building и district требуется выбор домов/участков if (formData.purposeType === 'building' && formData.purposeBuildingIds.length === 0) { newErrors.purposeBuildingIds = 'Выберите хотя бы один дом'; } if (formData.purposeType === 'district' && formData.purposeDistrictIds.length === 0) { newErrors.purposeDistrictIds = 'Выберите хотя бы один участок'; } // Для legal, office, hr, other не требуется выбор домов/участков if (formData.purposeType === 'other' && !formData.purposeDescription.trim()) { newErrors.purposeDescription = 'Укажите описание назначения'; } // Для event — описание или привязка к мероприятию if (formData.purposeType === 'event' && !formData.purposeDescription?.trim() && !formData.purposeEventId) { newErrors.purposeDescription = 'Укажите описание или выберите мероприятие'; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validate()) { return; } try { setLoading(true); const payload: Partial = { ...formData, createdBy: currentUserId }; if (formData.fromWorkPlan && formData.planItemId && formData.planItemBuildingId) { payload.planItemId = formData.planItemId; payload.planItemBuildingId = formData.planItemBuildingId; } else { payload.planItemId = undefined; payload.planItemBuildingId = undefined; } const needsDistribution = distributionBuildingIds.length > 1; if (needsDistribution && formData.itemType === 'service') { payload.distributionMethod = formData.distributionMethod ?? 'equal'; payload.distributionData = formData.distributionData ?? {}; } else { payload.distributionMethod = undefined; payload.distributionData = {}; } await onSave(payload); } catch (err) { console.error('Error saving invoice:', err); alert('Ошибка при сохранении счета'); } finally { setLoading(false); } }; const handleBuildingToggle = (buildingId: string) => { const current = formData.purposeBuildingIds; const newIds = current.includes(buildingId) ? current.filter(id => id !== buildingId) : [...current, buildingId]; setFormData({ ...formData, purposeBuildingIds: newIds, fromWorkPlan: false, planItemId: undefined, planItemBuildingId: undefined }); }; const handleDistrictToggle = (districtId: string) => { const current = formData.purposeDistrictIds; const newIds = current.includes(districtId) ? current.filter(id => id !== districtId) : [...current, districtId]; setFormData({ ...formData, purposeDistrictIds: newIds, fromWorkPlan: false, planItemId: undefined, planItemBuildingId: undefined }); }; // Обработчик загрузки файла const handleFileUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; setIsUploadingFile(true); try { const formDataUpload = new FormData(); formDataUpload.append('file', file); const response = await fetch('/api/finance/payment-invoices/upload', { method: 'POST', body: formDataUpload }); if (response.ok) { const result = await response.json(); const fileInfo = result.file || { url: result.url || result.fileUrl, filename: file.name }; // Добавляем файл в список setFormData(prev => ({ ...prev, fileUrls: [...(prev.fileUrls || []), fileInfo] })); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка загрузки файла: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка загрузки файла:', error); alert(`Ошибка загрузки файла: ${error.message || 'Неизвестная ошибка'}`); } finally { setIsUploadingFile(false); if (fileInputRef.current) { fileInputRef.current.value = ''; } } }; // Обработчики для услуг const addServiceItem = () => { const newItems = [...(formData.serviceItems || []), { name: '', amount: 0 }]; setFormData({ ...formData, serviceItems: newItems }); recalculateTotal(); }; const updateServiceItem = (index: number, field: keyof ServiceItem, value: string | number) => { const newItems = [...(formData.serviceItems || [])]; newItems[index] = { ...newItems[index], [field]: value }; setFormData({ ...formData, serviceItems: newItems }); recalculateTotal(); }; const removeServiceItem = (index: number) => { const newItems = formData.serviceItems?.filter((_, i) => i !== index) || []; setFormData({ ...formData, serviceItems: newItems.length > 0 ? newItems : undefined }); recalculateTotal(); }; // Обработчики для ТМЦ const addMaterialItem = () => { const newItems = [...(formData.materialItems || []), { name: '', quantity: 0, unit: 'шт', pricePerUnit: 0, amount: 0 }]; setFormData({ ...formData, materialItems: newItems }); recalculateTotal(); }; const updateMaterialItem = (index: number, field: keyof MaterialItem, value: string | number) => { const newItems = [...(formData.materialItems || [])]; const item = { ...newItems[index], [field]: value }; // Автоматически рассчитываем сумму при изменении количества или цены if (field === 'quantity' || field === 'pricePerUnit') { item.amount = item.quantity * item.pricePerUnit; } newItems[index] = item; setFormData({ ...formData, materialItems: newItems }); recalculateTotal(); }; const removeMaterialItem = (index: number) => { const newItems = formData.materialItems?.filter((_, i) => i !== index) || []; setFormData({ ...formData, materialItems: newItems.length > 0 ? newItems : undefined }); recalculateTotal(); }; // Пересчет общей суммы const recalculateTotal = () => { let total = 0; if (formData.itemType === 'service' && formData.serviceItems) { total = formData.serviceItems.reduce((sum, item) => sum + (item.amount || 0), 0); } else if (formData.itemType === 'materials' && formData.materialItems) { total = formData.materialItems.reduce((sum, item) => sum + (item.amount || 0), 0); } setFormData(prev => ({ ...prev, totalAmount: total })); }; // При изменении типа предмета счета - очищаем данные другого типа useEffect(() => { if (formData.itemType === 'service') { setFormData(prev => { if (!prev.serviceItems || prev.serviceItems.length === 0) { return { ...prev, materialItems: undefined, serviceItems: [] }; } return { ...prev, materialItems: undefined }; }); } else if (formData.itemType === 'materials') { setFormData(prev => { if (!prev.materialItems || prev.materialItems.length === 0) { return { ...prev, serviceItems: undefined, materialItems: [] }; } return { ...prev, serviceItems: undefined }; }); } // Пересчитываем сумму после изменения типа setTimeout(() => recalculateTotal(), 0); }, [formData.itemType]); return (
{/* Назначение счета */}

Назначение счета

{[ { value: 'building', label: 'Дом', icon: Building2 }, { value: 'district', label: 'Участок', icon: MapPin }, { value: 'legal', label: 'Юристы', icon: Briefcase }, { value: 'office', label: 'Офис', icon: FileText }, { value: 'hr', label: 'HR', icon: Users }, { value: 'event', label: 'Мероприятие', icon: PartyPopper }, { value: 'other', label: 'Другое', icon: FileText } ].map(({ value, label, icon: Icon }) => ( ))}
{/* Выбор домов */} {formData.purposeType === 'building' && (
{buildings.map(building => ( ))}
{errors.purposeBuildingIds && (

{errors.purposeBuildingIds}

)}
)} {/* Выбор участков */} {formData.purposeType === 'district' && (
{districts.map(district => ( ))}
{errors.purposeDistrictIds && (

{errors.purposeDistrictIds}

)}
)} {/* Из плана работ — при доме или участке */} {(formData.purposeType === 'building' || formData.purposeType === 'district') && (formData.purposeBuildingIds.length > 0 || formData.purposeDistrictIds.length > 0) && (
{formData.fromWorkPlan && planItemsForSelect.length > 0 && (
{planItemsForSelect.map(({ item, buildingId, buildingLabel }) => ( ))}
)} {formData.fromWorkPlan && planItemsForSelect.length === 0 && (

Нет пунктов плана на текущий год у выбранных домов. Добавьте работы в Участки → Дом → План работ.

)}
)} {/* Мероприятие: описание (название мероприятия) и привязка по ID */} {formData.purposeType === 'event' && (