245 lines
9.7 KiB
TypeScript
Executable File
245 lines
9.7 KiB
TypeScript
Executable File
import React, { useState, useEffect } from 'react';
|
||
import { Building, District } from '../../types';
|
||
import { backendApi } from '../../services/apiClient';
|
||
import { Calculator, Equal, Ruler, Edit3 } from 'lucide-react';
|
||
|
||
interface InvoiceDistributionProps {
|
||
purposeType: 'building' | 'district';
|
||
selectedBuildingIds: string[];
|
||
selectedDistrictIds: string[];
|
||
totalAmount: number;
|
||
distributionMethod: 'equal' | 'by_area' | 'manual' | null;
|
||
distributionData: Record<string, number>;
|
||
onDistributionChange: (method: 'equal' | 'by_area' | 'manual', data: Record<string, number>) => void;
|
||
}
|
||
|
||
export const InvoiceDistribution: React.FC<InvoiceDistributionProps> = ({
|
||
purposeType,
|
||
selectedBuildingIds,
|
||
selectedDistrictIds,
|
||
totalAmount,
|
||
distributionMethod,
|
||
distributionData,
|
||
onDistributionChange
|
||
}) => {
|
||
const [buildings, setBuildings] = useState<Building[]>([]);
|
||
const [districts, setDistricts] = useState<District[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [method, setMethod] = useState<'equal' | 'by_area' | 'manual'>(distributionMethod || 'equal');
|
||
const [manualAmounts, setManualAmounts] = useState<Record<string, number>>(distributionData || {});
|
||
|
||
useEffect(() => {
|
||
fetchData();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (method && totalAmount > 0) {
|
||
calculateDistribution();
|
||
}
|
||
}, [method, totalAmount, selectedBuildingIds, selectedDistrictIds, buildings]);
|
||
|
||
const fetchData = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const [buildingsData, districtsData] = await Promise.all([
|
||
backendApi.getBuildings(),
|
||
backendApi.getDistricts()
|
||
]);
|
||
setBuildings(buildingsData);
|
||
setDistricts(districtsData);
|
||
} catch (err) {
|
||
console.error('Error fetching data:', err);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const calculateDistribution = () => {
|
||
if (method === 'equal') {
|
||
const items = purposeType === 'building'
|
||
? selectedBuildingIds
|
||
: selectedDistrictIds;
|
||
const count = items.length;
|
||
if (count === 0) return;
|
||
|
||
const amountPerItem = totalAmount / count;
|
||
const rounded = Math.floor(amountPerItem * 100) / 100;
|
||
const remainder = totalAmount - (rounded * count);
|
||
|
||
const data: Record<string, number> = {};
|
||
items.forEach((id, index) => {
|
||
data[id] = rounded + (index === 0 ? remainder : 0);
|
||
});
|
||
|
||
onDistributionChange('equal', data);
|
||
setManualAmounts(data);
|
||
} else if (method === 'by_area' && purposeType === 'building') {
|
||
// Распределение по площади
|
||
const selectedBuildings = buildings.filter(b => selectedBuildingIds.includes(b.id));
|
||
const totalArea = selectedBuildings.reduce((sum, b) => {
|
||
const area = b.passport?.general?.totalArea || 0;
|
||
return sum + area;
|
||
}, 0);
|
||
|
||
if (totalArea === 0) {
|
||
// Если площади нет, распределяем поровну
|
||
calculateDistribution();
|
||
return;
|
||
}
|
||
|
||
const data: Record<string, number> = {};
|
||
selectedBuildings.forEach(building => {
|
||
const area = building.passport?.general?.totalArea || 0;
|
||
const proportion = area / totalArea;
|
||
data[building.id] = Math.round(totalAmount * proportion * 100) / 100;
|
||
});
|
||
|
||
// Корректируем остаток на первый элемент
|
||
const sum = Object.values(data).reduce((s, v) => s + v, 0);
|
||
const remainder = totalAmount - sum;
|
||
if (remainder !== 0 && selectedBuildings.length > 0) {
|
||
data[selectedBuildings[0].id] = (data[selectedBuildings[0].id] || 0) + remainder;
|
||
}
|
||
|
||
onDistributionChange('by_area', data);
|
||
setManualAmounts(data);
|
||
} else if (method === 'manual') {
|
||
// Для ручного ввода просто обновляем данные
|
||
onDistributionChange('manual', manualAmounts);
|
||
}
|
||
};
|
||
|
||
const handleManualAmountChange = (id: string, amount: number) => {
|
||
const newAmounts = { ...manualAmounts, [id]: amount };
|
||
setManualAmounts(newAmounts);
|
||
onDistributionChange('manual', newAmounts);
|
||
};
|
||
|
||
const getItemName = (id: string): string => {
|
||
if (purposeType === 'building') {
|
||
const building = buildings.find(b => b.id === id);
|
||
return building?.passport?.address || id;
|
||
} else {
|
||
const district = districts.find(d => d.id === id);
|
||
return district?.name || id;
|
||
}
|
||
};
|
||
|
||
const getItemArea = (id: string): number => {
|
||
if (purposeType === 'building') {
|
||
const building = buildings.find(b => b.id === id);
|
||
return building?.passport?.general?.totalArea || 0;
|
||
}
|
||
return 0;
|
||
};
|
||
|
||
if (loading) {
|
||
return <div className="p-4 text-center text-slate-500">Загрузка...</div>;
|
||
}
|
||
|
||
const items = purposeType === 'building' ? selectedBuildingIds : selectedDistrictIds;
|
||
const currentSum = Object.values(manualAmounts).reduce((sum, val) => sum + val, 0);
|
||
const difference = totalAmount - currentSum;
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<Calculator className="w-5 h-5 text-primary-600" />
|
||
<h4 className="font-bold text-slate-800">Распределение суммы</h4>
|
||
</div>
|
||
|
||
{/* Выбор метода распределения */}
|
||
<div className="grid grid-cols-3 gap-2">
|
||
<button
|
||
onClick={() => setMethod('equal')}
|
||
className={`p-3 rounded-lg border-2 transition-all ${
|
||
method === 'equal'
|
||
? 'border-primary-600 bg-primary-50 text-primary-700'
|
||
: 'border-slate-200 hover:border-primary-300 text-slate-700'
|
||
}`}
|
||
>
|
||
<Equal className="w-5 h-5 mx-auto mb-1" />
|
||
<span className="text-xs font-medium">Поровну</span>
|
||
</button>
|
||
{purposeType === 'building' && (
|
||
<button
|
||
onClick={() => setMethod('by_area')}
|
||
className={`p-3 rounded-lg border-2 transition-all ${
|
||
method === 'by_area'
|
||
? 'border-primary-600 bg-primary-50 text-primary-700'
|
||
: 'border-slate-200 hover:border-primary-300 text-slate-700'
|
||
}`}
|
||
>
|
||
<Ruler className="w-5 h-5 mx-auto mb-1" />
|
||
<span className="text-xs font-medium">По площади</span>
|
||
</button>
|
||
)}
|
||
<button
|
||
onClick={() => setMethod('manual')}
|
||
className={`p-3 rounded-lg border-2 transition-all ${
|
||
method === 'manual'
|
||
? 'border-primary-600 bg-primary-50 text-primary-700'
|
||
: 'border-slate-200 hover:border-primary-300 text-slate-700'
|
||
}`}
|
||
>
|
||
<Edit3 className="w-5 h-5 mx-auto mb-1" />
|
||
<span className="text-xs font-medium">Вручную</span>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Таблица распределения */}
|
||
<div className="bg-slate-50 rounded-lg p-4">
|
||
<div className="space-y-2 max-h-60 overflow-y-auto">
|
||
{items.map(id => (
|
||
<div key={id} className="flex items-center justify-between gap-4 p-2 bg-white rounded border border-slate-200">
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium text-slate-800 truncate">{getItemName(id)}</p>
|
||
{method === 'by_area' && purposeType === 'building' && (
|
||
<p className="text-xs text-slate-500">
|
||
Площадь: {getItemArea(id).toLocaleString('ru-RU')} м²
|
||
</p>
|
||
)}
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
type="number"
|
||
value={manualAmounts[id]?.toFixed(2) || '0.00'}
|
||
onChange={(e) => {
|
||
const value = parseFloat(e.target.value) || 0;
|
||
handleManualAmountChange(id, value);
|
||
}}
|
||
disabled={method !== 'manual'}
|
||
className="w-32 px-2 py-1 text-sm border border-slate-300 rounded focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-slate-100 disabled:text-slate-500"
|
||
step="0.01"
|
||
min="0"
|
||
/>
|
||
<span className="text-sm text-slate-600 font-medium">₽</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Итого и разница */}
|
||
<div className="mt-4 pt-4 border-t border-slate-300 space-y-1">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-slate-600">Итого распределено:</span>
|
||
<span className="font-bold text-slate-800">{currentSum.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-slate-600">Общая сумма:</span>
|
||
<span className="font-bold text-slate-800">{totalAmount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽</span>
|
||
</div>
|
||
{Math.abs(difference) > 0.01 && (
|
||
<div className={`flex justify-between text-sm font-bold ${
|
||
difference > 0 ? 'text-red-600' : 'text-emerald-600'
|
||
}`}>
|
||
<span>Разница:</span>
|
||
<span>{difference > 0 ? '+' : ''}{difference.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|