Files
mkd/components/building/InventoryView.tsx

942 lines
58 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
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<React.SetStateAction<Building>>, isEditing: boolean }> = ({ building, setBuilding, isEditing }) => {
const [searchTerm, setSearchTerm] = useState('');
const [filterCategory, setFilterCategory] = useState<'all' | BuildingInventoryItem['category']>('all');
const [writeOffModal, setWriteOffModal] = useState<BuildingInventoryItem | null>(null);
const [writeOffQty, setWriteOffQty] = useState(1);
const [writeOffReason, setWriteOffReason] = useState('Расходные материалы для ремонта');
const [writeOffUnitPrice, setWriteOffUnitPrice] = useState<number>(0); // Цена за единицу при списании
// При открытии модального окна списания устанавливаем цену из товара
useEffect(() => {
if (writeOffModal) {
setWriteOffUnitPrice(writeOffModal.unitPrice || 0);
setWriteOffQty(1);
}
}, [writeOffModal]);
// Модальное окно для импорта из счетов
const [showImportModal, setShowImportModal] = useState(false);
const [paymentInvoices, setPaymentInvoices] = useState<PaymentInvoice[]>([]);
const [loadingInvoices, setLoadingInvoices] = useState(false);
const [selectedInvoice, setSelectedInvoice] = useState<PaymentInvoice | null>(null);
// Map: индекс позиции -> количество для импорта
const [selectedMaterials, setSelectedMaterials] = useState<Map<number, number>>(new Map());
// Модальное окно для взятия со склада участка
const [showDistrictWarehouseModal, setShowDistrictWarehouseModal] = useState(false);
const [districtInventory, setDistrictInventory] = useState<BuildingInventoryItem[]>([]);
const [selectedDistrictItems, setSelectedDistrictItems] = useState<Map<string, number>>(new Map()); // itemId -> quantity
const [district, setDistrict] = useState<District | null>(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 (
<div className="space-y-6 animate-fade-in">
{/* Stats Header */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<p className="text-[10px] text-slate-400 font-bold uppercase">Всего позиций</p>
<p className="text-xl font-black text-slate-800">{inventory.length}</p>
</div>
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<p className="text-[10px] text-slate-400 font-bold uppercase">Списано в этом мес.</p>
<p className="text-xl font-black text-red-600">{(building.writeOffHistory || []).length}</p>
</div>
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<p className="text-[10px] text-slate-400 font-bold uppercase">Сумма списаний</p>
<p className="text-xl font-black text-red-600">
{(building.writeOffHistory || []).reduce((sum, wo) => sum + (wo.totalAmount || 0), 0).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</p>
</div>
</div>
{/* Filter Bar */}
<div className="flex gap-2 overflow-x-auto no-scrollbar pb-2">
{categories.map(cat => (
<button
key={cat.id}
onClick={() => setFilterCategory(cat.id as any)}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-bold whitespace-nowrap transition-all ${filterCategory === cat.id ? 'bg-primary-600 text-white shadow-md shadow-primary-200' : 'bg-white text-slate-500 border border-slate-200 hover:border-primary-300'}`}
>
<cat.icon className="w-3.5 h-3.5"/>
{cat.label}
</button>
))}
</div>
{/* Search and Add Button */}
<div className="flex gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Поиск по инвентарю..."
value={searchTerm}
onChange={(e) => 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"
/>
</div>
<button
onClick={() => setShowDistrictWarehouseModal(true)}
className="px-4 py-2.5 bg-emerald-600 text-white rounded-xl font-bold text-xs flex items-center gap-2 hover:bg-emerald-700 transition-all shadow-md shadow-emerald-200"
>
<Warehouse className="w-4 h-4"/> Взять со склада участка
</button>
<button
onClick={() => setShowImportModal(true)}
className="px-4 py-2.5 bg-primary-600 text-white rounded-xl font-bold text-xs flex items-center gap-2 hover:bg-primary-700 transition-all shadow-md shadow-primary-200"
>
<FileText className="w-4 h-4"/> Добавить из счета
</button>
</div>
{/* Inventory List */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="bg-slate-50 text-slate-500 font-bold border-b border-slate-200 text-[10px] uppercase tracking-wider">
<tr>
<th className="p-4">Наименование</th>
<th className="p-4 text-center">Кол-во</th>
<th className="p-4 text-center">Цена за ед.</th>
<th className="p-4 text-center">Общая стоимость</th>
<th className="p-4 text-center">Источник</th>
<th className="p-4 text-right">Действие</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filteredItems.map(item => (
<tr key={item.id} className="hover:bg-slate-50 transition-colors">
<td className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-100 rounded-lg text-slate-400">
{item.category === 'tool' ? <Hammer className="w-4 h-4"/> : item.category === 'door' ? <DoorOpen className="w-4 h-4"/> : <Package className="w-4 h-4"/>}
</div>
<div>
<p className="font-bold text-slate-700">{item.name}</p>
<p className="text-[10px] text-slate-400 uppercase">{item.category}</p>
</div>
</div>
</td>
<td className="p-4 text-center">
<span className={`px-2 py-1 rounded text-xs font-bold ${item.quantity < 3 && item.category === 'consumable' ? 'bg-red-50 text-red-600' : 'bg-slate-100 text-slate-700'}`}>
{item.quantity} {item.unit}
</span>
</td>
<td className="p-4 text-center">
{item.unitPrice ? (
<span className="px-2 py-1 rounded text-xs font-bold bg-blue-50 text-blue-700">
{item.unitPrice.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} /{item.unit}
</span>
) : (
<span className="text-xs text-slate-400"></span>
)}
</td>
<td className="p-4 text-center">
{item.totalAmount ? (
<span className="px-2 py-1 rounded text-xs font-bold bg-green-50 text-green-700">
{item.totalAmount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
) : (
<span className="text-xs text-slate-400"></span>
)}
</td>
<td className="p-4 text-center">
<span className={`px-2 py-1 rounded text-xs font-bold ${
item.source === 'district'
? 'bg-emerald-50 text-emerald-700'
: item.source === 'invoice'
? 'bg-blue-50 text-blue-700'
: 'bg-slate-100 text-slate-500'
}`}>
{item.source === 'district' ? 'Склад участка' : item.source === 'invoice' ? 'Из счета' : 'Не указан'}
</span>
</td>
<td className="p-4 text-right">
<button
onClick={() => setWriteOffModal(item)}
className="text-[10px] font-bold text-red-600 bg-red-50 px-3 py-1.5 rounded-lg border border-red-100 hover:bg-red-100 transition-colors flex items-center gap-1 ml-auto"
>
<ArrowDownToLine className="w-3.5 h-3.5"/> Списать
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{filteredItems.length === 0 && (
<div className="p-12 text-center text-slate-400 italic">Позиции не найдены</div>
)}
</div>
{/* Write-off History */}
<div>
<h3 className="font-bold text-slate-700 text-sm mb-3 px-1 flex items-center gap-2">
<History className="w-4 h-4"/> История списаний
</h3>
<div className="space-y-3">
{(building.writeOffHistory || []).map(wo => (
<div key={wo.id} className="bg-slate-50 p-3 rounded-xl border border-slate-100 text-xs">
<div className="flex justify-between items-start mb-2">
<div className="flex-1">
<span className="font-bold text-slate-800">{wo.items[0].name}</span>
<span className="text-slate-500 ml-2">({wo.items[0].quantity} {wo.items[0].unit})</span>
</div>
<div className="text-right">
<div className="font-black text-red-600 text-sm">
{wo.totalAmount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
<div className="text-slate-400 font-mono text-[10px] mt-1">{wo.date}</div>
</div>
</div>
<div className="flex justify-between items-center mt-2">
<p className="text-slate-500 italic text-[11px]">Причина: {wo.items[0].reason || wo.reason || 'Не указана'}</p>
<p className="text-[10px] text-slate-400">Отв: {wo.performer}</p>
</div>
{wo.items[0].unitPrice > 0 && (
<p className="text-[10px] text-slate-400 mt-1">
Цена: {wo.items[0].unitPrice.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} /{wo.items[0].unit}
</p>
)}
</div>
))}
{(building.writeOffHistory || []).length === 0 && (
<p className="text-center text-slate-400 italic text-xs py-4">Списаний еще не было</p>
)}
</div>
</div>
{/* Write-off Modal */}
{writeOffModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in" onClick={() => {
setWriteOffModal(null);
setWriteOffQty(1);
setWriteOffUnitPrice(writeOffModal.unitPrice || 0);
}}>
<div className="bg-white rounded-2xl w-full max-w-sm p-6 shadow-2xl animate-slide-up" onClick={e => e.stopPropagation()}>
<h3 className="text-lg font-bold text-slate-800 mb-2">Списание со склада дома</h3>
<p className="text-sm text-slate-500 mb-6">{writeOffModal.name}</p>
<div className="space-y-4">
<div>
<label className="block text-xs font-bold text-slate-400 uppercase mb-1">Количество</label>
<div className="flex items-center gap-4">
<input
type="range" min="1" max={writeOffModal.quantity}
value={writeOffQty}
onChange={e => setWriteOffQty(Number(e.target.value))}
className="flex-1 accent-primary-600"
/>
<span className="text-lg font-black text-slate-800 w-12 text-center">{writeOffQty}</span>
</div>
</div>
<div>
<label className="block text-xs font-bold text-slate-400 uppercase mb-1">Цена за единицу ()</label>
<input
type="number"
min="0"
step="0.01"
value={writeOffUnitPrice}
onChange={e => 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 && (
<p className="text-xs text-slate-400 mt-1">Текущая цена: {writeOffModal.unitPrice.toLocaleString('ru-RU')} </p>
)}
</div>
<div className="bg-red-50 p-3 rounded-xl border border-red-100">
<div className="flex justify-between items-center">
<span className="text-xs font-bold text-red-700 uppercase">Сумма списания:</span>
<span className="text-lg font-black text-red-700">
{(writeOffQty * writeOffUnitPrice).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
</div>
</div>
<div>
<label className="block text-xs font-bold text-slate-400 uppercase mb-1">Причина списания</label>
<textarea
value={writeOffReason}
onChange={e => setWriteOffReason(e.target.value)}
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 min-h-[80px]"
/>
</div>
</div>
<div className="mt-8 flex gap-3">
<button onClick={() => {
setWriteOffModal(null);
setWriteOffQty(1);
setWriteOffUnitPrice(writeOffModal.unitPrice || 0);
}} className="flex-1 py-3 text-slate-600 font-bold bg-slate-100 rounded-xl">Отмена</button>
<button onClick={handleWriteOff} className="flex-1 py-3 text-white font-bold bg-red-600 rounded-xl hover:bg-red-700">Списать</button>
</div>
</div>
</div>
)}
{/* Import from Invoice Modal */}
{showImportModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in" onClick={() => setShowImportModal(false)}>
<div className="bg-white rounded-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden shadow-2xl animate-slide-up flex flex-col" onClick={e => e.stopPropagation()}>
<div className="p-6 border-b border-slate-200 flex justify-between items-center">
<div>
<h3 className="text-lg font-bold text-slate-800">Добавить инвентарь из счета</h3>
<p className="text-sm text-slate-500 mt-1">Выберите счет с ТМЦ и позиции для добавления</p>
</div>
<button onClick={() => setShowImportModal(false)} className="p-2 hover:bg-slate-100 rounded-full">
<X className="w-5 h-5 text-slate-400"/>
</button>
</div>
<div className="flex-1 overflow-auto p-6">
{loadingInvoices ? (
<div className="text-center py-12 text-slate-400">Загрузка счетов...</div>
) : paymentInvoices.length === 0 ? (
<div className="text-center py-12 text-slate-400">
<FileText className="w-12 h-12 mx-auto mb-3 opacity-20"/>
<p>Нет счетов с ТМЦ для этого дома</p>
</div>
) : !selectedInvoice ? (
<div className="space-y-3">
<h4 className="font-bold text-slate-700 text-sm mb-4">Выберите счет:</h4>
{paymentInvoices.map((inv) => (
<div
key={inv.id}
onClick={() => setSelectedInvoice(inv)}
className="p-4 border border-slate-200 rounded-xl hover:border-primary-400 hover:bg-primary-50/20 cursor-pointer transition-all"
>
<div className="flex justify-between items-start">
<div>
<div className="font-bold text-slate-800">{inv.invoiceNumber}</div>
<div className="text-sm text-slate-600 mt-1">{inv.contractorName}</div>
<div className="text-xs text-slate-400 mt-1">
{inv.materialItems?.length || 0} позиций ТМЦ {new Date(inv.createdAt).toLocaleDateString('ru-RU')}
</div>
</div>
<div className="text-right">
<div className="font-black text-slate-800">{Number(inv.totalAmount || 0).toLocaleString('ru-RU')} </div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="space-y-4">
<div className="flex items-center justify-between pb-4 border-b border-slate-200">
<div>
<h4 className="font-bold text-slate-800">{selectedInvoice.invoiceNumber}</h4>
<p className="text-sm text-slate-500">{selectedInvoice.contractorName}</p>
</div>
<button
onClick={() => {
setSelectedInvoice(null);
setSelectedMaterials(new Map());
}}
className="text-xs text-slate-500 hover:text-slate-700 flex items-center gap-1"
>
<X className="w-4 h-4"/> Выбрать другой счет
</button>
</div>
<div className="space-y-2">
<h5 className="font-bold text-slate-700 text-sm">Выберите позиции и укажите количество:</h5>
{selectedInvoice.materialItems?.map((material, index) => {
const isSelected = selectedMaterials.has(index);
const selectedQty = selectedMaterials.get(index) || 0;
return (
<div
key={index}
className={`p-4 border-2 rounded-xl transition-all ${
isSelected
? 'border-primary-500 bg-primary-50'
: 'border-slate-200 hover:border-slate-300'
}`}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3 flex-1">
<div
onClick={() => toggleMaterialSelection(index, material)}
className={`w-5 h-5 rounded border-2 flex items-center justify-center flex-shrink-0 mt-0.5 cursor-pointer ${
isSelected
? 'bg-primary-500 border-primary-500'
: 'border-slate-300'
}`}
>
{isSelected && (
<Check className="w-3 h-3 text-white"/>
)}
</div>
<div className="flex-1">
<div className="font-bold text-slate-800">{material.name}</div>
<div className="text-sm text-slate-500 mt-1">
В счете: {material.quantity} {material.unit} × {material.pricePerUnit.toLocaleString('ru-RU')} = {material.amount.toLocaleString('ru-RU')}
</div>
</div>
</div>
{isSelected && (
<div className="flex items-center gap-2 flex-shrink-0" onClick={(e) => e.stopPropagation()}>
<label className="text-xs text-slate-500 font-bold whitespace-nowrap">Кол-во:</label>
<input
type="number"
min="1"
max={material.quantity}
value={selectedQty}
onChange={(e) => {
const val = Number(e.target.value);
updateMaterialQuantity(index, val, material.quantity);
}}
onClick={(e) => e.stopPropagation()}
className="w-20 px-2 py-1 border border-slate-300 rounded-lg text-sm font-bold text-slate-800 focus:outline-none focus:ring-2 focus:ring-primary-500"
/>
<span className="text-xs text-slate-500">{material.unit}</span>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
</div>
{selectedInvoice && selectedMaterials.size > 0 && (
<div className="p-6 border-t border-slate-200 bg-slate-50 flex justify-between items-center">
<div className="text-sm text-slate-600">
Выбрано позиций: <span className="font-bold text-slate-800">{selectedMaterials.size}</span>
{' • '}
Всего единиц: <span className="font-bold text-slate-800">
{Array.from(selectedMaterials.values()).reduce((sum, qty) => sum + qty, 0)}
</span>
</div>
<div className="flex gap-3">
<button
onClick={() => {
setShowImportModal(false);
setSelectedInvoice(null);
setSelectedMaterials(new Map());
}}
className="px-4 py-2 text-slate-600 font-bold bg-white border border-slate-200 rounded-xl hover:bg-slate-50"
>
Отмена
</button>
<button
onClick={handleImportMaterials}
className="px-4 py-2 bg-primary-600 text-white font-bold rounded-xl hover:bg-primary-700 flex items-center gap-2"
>
<Plus className="w-4 h-4"/> Добавить выбранные
</button>
</div>
</div>
)}
</div>
</div>
)}
{/* Take from District Warehouse Modal */}
{showDistrictWarehouseModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in" onClick={() => setShowDistrictWarehouseModal(false)}>
<div className="bg-white rounded-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden shadow-2xl animate-slide-up flex flex-col" onClick={e => e.stopPropagation()}>
<div className="p-6 border-b border-slate-200 flex justify-between items-center">
<div>
<h3 className="text-lg font-bold text-slate-800">Взять со склада участка</h3>
<p className="text-sm text-slate-500 mt-1">
{district ? `Склад участка: ${district.name}` : 'Участок не найден'}
</p>
</div>
<button onClick={() => setShowDistrictWarehouseModal(false)} className="p-2 hover:bg-slate-100 rounded-full">
<X className="w-5 h-5 text-slate-400"/>
</button>
</div>
<div className="flex-1 overflow-auto p-6">
{!district ? (
<div className="text-center py-12 text-slate-400">
<Warehouse className="w-12 h-12 mx-auto mb-3 opacity-20"/>
<p>Участок не найден</p>
</div>
) : districtInventory.length === 0 ? (
<div className="text-center py-12 text-slate-400">
<Warehouse className="w-12 h-12 mx-auto mb-3 opacity-20"/>
<p>Склад участка пуст</p>
</div>
) : (
<div className="space-y-2">
<h5 className="font-bold text-slate-700 text-sm mb-4">Выберите позиции и укажите количество:</h5>
{districtInventory.map((item) => {
const isSelected = selectedDistrictItems.has(item.id);
const selectedQty = selectedDistrictItems.get(item.id) || 0;
return (
<div
key={item.id}
className={`p-4 border-2 rounded-xl transition-all ${
isSelected
? 'border-emerald-500 bg-emerald-50'
: 'border-slate-200 hover:border-slate-300'
}`}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3 flex-1">
<div
onClick={() => toggleDistrictItemSelection(item.id, item)}
className={`w-5 h-5 rounded border-2 flex items-center justify-center flex-shrink-0 mt-0.5 cursor-pointer ${
isSelected
? 'bg-emerald-500 border-emerald-500'
: 'border-slate-300'
}`}
>
{isSelected && (
<Check className="w-3 h-3 text-white"/>
)}
</div>
<div className="flex-1">
<div className="font-bold text-slate-800">{item.name}</div>
<div className="text-sm text-slate-500 mt-1">
На складе: {item.quantity} {item.unit}
</div>
{item.unitPrice && (
<div className="text-xs text-blue-600 mt-1">
Цена: {item.unitPrice.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} /{item.unit}
</div>
)}
{item.totalAmount && (
<div className="text-xs text-green-600 mt-1 font-bold">
Общая стоимость: {item.totalAmount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
)}
<div className="text-xs text-slate-400 mt-1 uppercase">
{item.category}
</div>
</div>
</div>
{isSelected && (
<div className="flex items-center gap-2 flex-shrink-0" onClick={(e) => e.stopPropagation()}>
<label className="text-xs text-slate-500 font-bold whitespace-nowrap">Кол-во:</label>
<input
type="number"
min="1"
max={item.quantity}
value={selectedQty}
onChange={(e) => {
const val = Number(e.target.value);
updateDistrictItemQuantity(item.id, val, item.quantity);
}}
onClick={(e) => e.stopPropagation()}
className="w-20 px-2 py-1 border border-slate-300 rounded-lg text-sm font-bold text-slate-800 focus:outline-none focus:ring-2 focus:ring-emerald-500"
/>
<span className="text-xs text-slate-500">{item.unit}</span>
</div>
)}
</div>
</div>
);
})}
</div>
)}
</div>
{district && selectedDistrictItems.size > 0 && (
<div className="p-6 border-t border-slate-200 bg-slate-50 flex justify-between items-center">
<div className="text-sm text-slate-600">
Выбрано позиций: <span className="font-bold text-slate-800">{selectedDistrictItems.size}</span>
{' • '}
Всего единиц: <span className="font-bold text-slate-800">
{Array.from(selectedDistrictItems.values()).reduce((sum, qty) => sum + qty, 0)}
</span>
</div>
<div className="flex gap-3">
<button
onClick={() => {
setShowDistrictWarehouseModal(false);
setSelectedDistrictItems(new Map());
}}
className="px-4 py-2 text-slate-600 font-bold bg-white border border-slate-200 rounded-xl hover:bg-slate-50"
>
Отмена
</button>
<button
onClick={handleTakeFromDistrict}
className="px-4 py-2 bg-emerald-600 text-white font-bold rounded-xl hover:bg-emerald-700 flex items-center gap-2"
>
<Warehouse className="w-4 h-4"/> Взять выбранные
</button>
</div>
</div>
)}
</div>
</div>
)}
</div>
);
};