Files
mkd/components/finance/InvoiceDistribution.tsx

245 lines
9.7 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
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>
);
};