Files
mkd/components/finance/InvoiceDistribution.tsx
2026-02-04 00:17:04 +05:00

245 lines
9.7 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, 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>
);
};