Files
mkd/components/building/InventoryView.tsx
2026-02-04 00:17:04 +05:00

942 lines
58 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 } 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>
);
};