import React, { useState, useEffect } from 'react'; import { Building, BuildingInventoryItem, PaymentInvoice, MaterialItem, District, WriteOffAct, WriteOffItem } from '../../types'; import { Package, Hammer, DoorOpen, Droplets, Trash2, Plus, ArrowDownToLine, History, Search, FileText, Check, X, Warehouse } from 'lucide-react'; import { apiClient, backendApi } from '../../services/apiClient'; export const InventoryView: React.FC<{ building: Building, setBuilding: React.Dispatch>, isEditing: boolean }> = ({ building, setBuilding, isEditing }) => { const [searchTerm, setSearchTerm] = useState(''); const [filterCategory, setFilterCategory] = useState<'all' | BuildingInventoryItem['category']>('all'); const [writeOffModal, setWriteOffModal] = useState(null); const [writeOffQty, setWriteOffQty] = useState(1); const [writeOffReason, setWriteOffReason] = useState('Расходные материалы для ремонта'); const [writeOffUnitPrice, setWriteOffUnitPrice] = useState(0); // Цена за единицу при списании // При открытии модального окна списания устанавливаем цену из товара useEffect(() => { if (writeOffModal) { setWriteOffUnitPrice(writeOffModal.unitPrice || 0); setWriteOffQty(1); } }, [writeOffModal]); // Модальное окно для импорта из счетов const [showImportModal, setShowImportModal] = useState(false); const [paymentInvoices, setPaymentInvoices] = useState([]); const [loadingInvoices, setLoadingInvoices] = useState(false); const [selectedInvoice, setSelectedInvoice] = useState(null); // Map: индекс позиции -> количество для импорта const [selectedMaterials, setSelectedMaterials] = useState>(new Map()); // Модальное окно для взятия со склада участка const [showDistrictWarehouseModal, setShowDistrictWarehouseModal] = useState(false); const [districtInventory, setDistrictInventory] = useState([]); const [selectedDistrictItems, setSelectedDistrictItems] = useState>(new Map()); // itemId -> quantity const [district, setDistrict] = useState(null); const inventory = building.inventory || []; const filteredItems = inventory.filter(item => { const matchesSearch = item.name.toLowerCase().includes(searchTerm.toLowerCase()); const matchesFilter = filterCategory === 'all' || item.category === filterCategory; return matchesSearch && matchesFilter; }); const handleWriteOff = () => { if (!writeOffModal) return; // Используем цену из модального окна (или из товара, если не указана) const unitPrice = writeOffUnitPrice || writeOffModal.unitPrice || 0; const itemAmount = writeOffQty * unitPrice; const writeOffItem: WriteOffItem = { itemId: writeOffModal.id, name: writeOffModal.name, quantity: writeOffQty, unit: writeOffModal.unit, unitPrice: unitPrice, amount: itemAmount, reason: writeOffReason }; const act: WriteOffAct = { id: `wo-${Date.now()}`, date: new Date().toISOString().split('T')[0], items: [writeOffItem], totalAmount: itemAmount, // Общая сумма списания performer: 'Мастер участка', reason: writeOffReason }; setBuilding(prev => ({ ...prev, inventory: prev.inventory.map(item => item.id === writeOffModal.id ? { ...item, quantity: Math.max(0, item.quantity - writeOffQty) } : item ), writeOffHistory: [act, ...(prev.writeOffHistory || [])], isDirty: true })); setWriteOffModal(null); }; // Загрузка счетов с ТМЦ для текущего дома useEffect(() => { if (!showImportModal) return; const loadInvoices = async () => { try { setLoadingInvoices(true); const resp = await apiClient.get<{ invoices: PaymentInvoice[] }>( `/finance/payment-invoices?purposeType=building&limit=200` ); // Фильтруем: только по текущему дому и только с ТМЦ const filtered = (resp?.invoices || []).filter((inv) => Array.isArray(inv.purposeBuildingIds) && inv.purposeBuildingIds.includes(building.id) && inv.itemType === 'materials' && inv.materialItems && inv.materialItems.length > 0 ); setPaymentInvoices(filtered); } catch (e) { console.warn('[InventoryView] Failed to load payment invoices:', e); setPaymentInvoices([]); } finally { setLoadingInvoices(false); } }; loadInvoices(); }, [showImportModal, building.id]); // Загрузка склада участка useEffect(() => { if (!showDistrictWarehouseModal || !building.districtId) return; const loadDistrict = async () => { try { const districts = await backendApi.getDistricts(); const currentDistrict = districts.find(d => d.id === building.districtId); if (currentDistrict) { setDistrict(currentDistrict); setDistrictInventory(currentDistrict.inventory || []); } else { setDistrict(null); setDistrictInventory([]); } } catch (error) { console.error('Ошибка при загрузке участка:', error); setDistrict(null); setDistrictInventory([]); } setSelectedDistrictItems(new Map()); }; loadDistrict(); }, [showDistrictWarehouseModal, building.districtId]); // Добавление выбранных позиций на склад участка (вместо прямого добавления в дом) const handleImportMaterials = async () => { if (!selectedInvoice || selectedMaterials.size === 0 || !building.districtId) return; try { // Получаем участок с сервера const districts = await backendApi.getDistricts(); const currentDistrict = districts.find(d => d.id === building.districtId); if (!currentDistrict) { alert('Участок не найден'); return; } const newItems: BuildingInventoryItem[] = Array.from(selectedMaterials.entries()) .map(([index, quantity]) => { const material = selectedInvoice.materialItems![index]; if (!material || quantity <= 0) return null; // Определяем категорию по названию const nameLower = material.name.toLowerCase(); let category: BuildingInventoryItem['category'] = 'material'; if (nameLower.includes('инструмент') || nameLower.includes('дрель') || nameLower.includes('перфоратор') || nameLower.includes('болгарка') || nameLower.includes('отвертка') || nameLower.includes('ключ')) { category = 'tool'; } else if (nameLower.includes('дверь') || nameLower.includes('окно') || nameLower.includes('рама') || nameLower.includes('блок')) { category = 'door'; } else if (nameLower.includes('расход') || nameLower.includes('салфетк') || nameLower.includes('перчатк') || nameLower.includes('скотч')) { category = 'consumable'; } // Рассчитываем общую стоимость для выбранного количества const totalAmount = quantity * material.pricePerUnit; return { id: `inv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, name: material.name, category, quantity: quantity, unit: material.unit, lastCheck: new Date().toISOString().split('T')[0], source: 'invoice' as const, unitPrice: material.pricePerUnit, // Цена за единицу (стоимость / количество) totalAmount: totalAmount // Общая стоимость товара }; }) .filter((item): item is BuildingInventoryItem => item !== null); // Добавляем на склад участка (объединяем с существующими позициями) const existingInventory = currentDistrict.inventory || []; const updatedInventory = [...existingInventory]; newItems.forEach(newItem => { // Ищем существующую позицию с таким же названием и единицей измерения const existingIndex = updatedInventory.findIndex( item => item.name === newItem.name && item.unit === newItem.unit ); if (existingIndex >= 0) { // Объединяем с существующей позицией const existing = updatedInventory[existingIndex]; const newQuantity = existing.quantity + newItem.quantity; const newTotalAmount = (existing.totalAmount || 0) + (newItem.totalAmount || 0); // Пересчитываем цену за единицу на основе общей стоимости const newUnitPrice = newQuantity > 0 ? newTotalAmount / newQuantity : (newItem.unitPrice || existing.unitPrice || 0); updatedInventory[existingIndex] = { ...existing, quantity: newQuantity, totalAmount: newTotalAmount, unitPrice: newUnitPrice }; } else { // Добавляем новую позицию updatedInventory.push(newItem); } }); // Сохраняем обновленный склад участка через API await backendApi.updateDistrict(currentDistrict.id, { inventory: updatedInventory }); alert(`Добавлено ${newItems.length} позиций на склад участка "${currentDistrict.name}"`); // Закрываем модальное окно и сбрасываем состояние setShowImportModal(false); setSelectedInvoice(null); setSelectedMaterials(new Map()); } catch (error) { console.error('Ошибка при сохранении на склад участка:', error); alert('Ошибка при сохранении на склад участка'); } }; const toggleMaterialSelection = (index: number, material: MaterialItem) => { setSelectedMaterials(prev => { const newMap = new Map(prev); if (newMap.has(index)) { newMap.delete(index); } else { // По умолчанию ставим максимальное количество из счета newMap.set(index, material.quantity); } return newMap; }); }; const updateMaterialQuantity = (index: number, quantity: number, maxQuantity: number) => { const validQuantity = Math.max(0, Math.min(quantity, maxQuantity)); setSelectedMaterials(prev => { const newMap = new Map(prev); if (validQuantity > 0) { newMap.set(index, validQuantity); } else { newMap.delete(index); } return newMap; }); }; // Взятие позиций со склада участка в дом const handleTakeFromDistrict = async () => { if (!district || selectedDistrictItems.size === 0) return; try { const itemsToAdd: BuildingInventoryItem[] = []; const updatedDistrictInventory = [...districtInventory]; selectedDistrictItems.forEach((quantity, itemId) => { const districtItem = districtInventory.find(item => item.id === itemId); if (!districtItem || quantity <= 0 || quantity > districtItem.quantity) return; // Добавляем в дом const existingItemIndex = building.inventory.findIndex( item => item.name === districtItem.name && item.unit === districtItem.unit ); if (existingItemIndex >= 0) { // Объединяем с существующей позицией в доме (пересчитываем общую стоимость и цену) setBuilding(prev => { const existing = prev.inventory[existingItemIndex]; const newQuantity = existing.quantity + quantity; const itemTotalAmount = quantity * (districtItem.unitPrice || 0); const newTotalAmount = (existing.totalAmount || 0) + itemTotalAmount; const newUnitPrice = newQuantity > 0 ? newTotalAmount / newQuantity : (districtItem.unitPrice || existing.unitPrice || 0); return { ...prev, inventory: prev.inventory.map((item, idx) => idx === existingItemIndex ? { ...item, quantity: newQuantity, totalAmount: newTotalAmount, unitPrice: newUnitPrice } : item ), isDirty: true }; }); } else { // Добавляем новую позицию в дом (сохраняем цену и общую стоимость) const itemTotalAmount = quantity * (districtItem.unitPrice || 0); itemsToAdd.push({ ...districtItem, id: `inv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, quantity: quantity, source: 'district' as const, unitPrice: districtItem.unitPrice, // Сохраняем цену при переносе со склада участка totalAmount: itemTotalAmount // Рассчитываем общую стоимость для перенесенного количества }); } // Уменьшаем количество на складе участка и пересчитываем общую стоимость const districtItemIndex = updatedDistrictInventory.findIndex(item => item.id === itemId); if (districtItemIndex >= 0) { const existing = updatedDistrictInventory[districtItemIndex]; const newQuantity = existing.quantity - quantity; if (newQuantity > 0) { // Пересчитываем общую стоимость пропорционально оставшемуся количеству const unitPrice = existing.unitPrice || 0; const newTotalAmount = newQuantity * unitPrice; updatedDistrictInventory[districtItemIndex] = { ...existing, quantity: newQuantity, totalAmount: newTotalAmount }; } else { updatedDistrictInventory.splice(districtItemIndex, 1); } } }); // Обновляем склад участка через API await backendApi.updateDistrict(district.id, { inventory: updatedDistrictInventory }); setDistrictInventory(updatedDistrictInventory); // Добавляем новые позиции в дом if (itemsToAdd.length > 0) { setBuilding(prev => ({ ...prev, inventory: [...prev.inventory, ...itemsToAdd], isDirty: true })); } // Закрываем модальное окно setShowDistrictWarehouseModal(false); setSelectedDistrictItems(new Map()); } catch (error) { console.error('Ошибка при взятии со склада участка:', error); alert('Ошибка при взятии со склада участка'); } }; const toggleDistrictItemSelection = (itemId: string, item: BuildingInventoryItem) => { setSelectedDistrictItems(prev => { const newMap = new Map(prev); if (newMap.has(itemId)) { newMap.delete(itemId); } else { newMap.set(itemId, item.quantity); } return newMap; }); }; const updateDistrictItemQuantity = (itemId: string, quantity: number, maxQuantity: number) => { const validQuantity = Math.max(0, Math.min(quantity, maxQuantity)); setSelectedDistrictItems(prev => { const newMap = new Map(prev); if (validQuantity > 0) { newMap.set(itemId, validQuantity); } else { newMap.delete(itemId); } return newMap; }); }; const categories = [ { id: 'all', label: 'Все', icon: Package }, { id: 'material', label: 'Материалы', icon: Droplets }, { id: 'tool', label: 'Инструменты', icon: Hammer }, { id: 'door', label: 'Двери/Окна', icon: DoorOpen }, { id: 'consumable', label: 'Расходники', icon: Package }, ]; return (
{/* Stats Header */}

Всего позиций

{inventory.length}

Списано в этом мес.

{(building.writeOffHistory || []).length}

Сумма списаний

{(building.writeOffHistory || []).reduce((sum, wo) => sum + (wo.totalAmount || 0), 0).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽

{/* Filter Bar */}
{categories.map(cat => ( ))}
{/* Search and Add Button */}
setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-slate-200 focus:outline-none focus:ring-2 focus:ring-primary-500 text-sm bg-white" />
{/* Inventory List */}
{filteredItems.map(item => ( ))}
Наименование Кол-во Цена за ед. Общая стоимость Источник Действие
{item.category === 'tool' ? : item.category === 'door' ? : }

{item.name}

{item.category}

{item.quantity} {item.unit} {item.unitPrice ? ( {item.unitPrice.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽/{item.unit} ) : ( )} {item.totalAmount ? ( {item.totalAmount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽ ) : ( )} {item.source === 'district' ? 'Склад участка' : item.source === 'invoice' ? 'Из счета' : 'Не указан'}
{filteredItems.length === 0 && (
Позиции не найдены
)}
{/* Write-off History */}

История списаний

{(building.writeOffHistory || []).map(wo => (
{wo.items[0].name} ({wo.items[0].quantity} {wo.items[0].unit})
{wo.totalAmount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽
{wo.date}

Причина: {wo.items[0].reason || wo.reason || 'Не указана'}

Отв: {wo.performer}

{wo.items[0].unitPrice > 0 && (

Цена: {wo.items[0].unitPrice.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽/{wo.items[0].unit}

)}
))} {(building.writeOffHistory || []).length === 0 && (

Списаний еще не было

)}
{/* Write-off Modal */} {writeOffModal && (
{ setWriteOffModal(null); setWriteOffQty(1); setWriteOffUnitPrice(writeOffModal.unitPrice || 0); }}>
e.stopPropagation()}>

Списание со склада дома

{writeOffModal.name}

setWriteOffQty(Number(e.target.value))} className="flex-1 accent-primary-600" /> {writeOffQty}
setWriteOffUnitPrice(Number(e.target.value))} placeholder={writeOffModal.unitPrice ? writeOffModal.unitPrice.toString() : "0.00"} className="w-full p-3 bg-slate-50 rounded-xl border border-slate-200 text-sm outline-none focus:ring-2 focus:ring-primary-500" /> {writeOffModal.unitPrice && (

Текущая цена: {writeOffModal.unitPrice.toLocaleString('ru-RU')} ₽

)}
Сумма списания: {(writeOffQty * writeOffUnitPrice).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽