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

1184 lines
82 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, useMemo, useEffect, useCallback } from 'react';
import { Building, PlanItem, InspectionAct, InspectionSection, InspectionCommonSection, InspectionMKDElement, PaymentInvoice } from '../../types';
import { apiClient } from '../../services/apiClient';
import {
Calendar,
Wallet,
CheckCircle2,
Clock,
Plus,
Trash2,
PieChart,
Upload,
ArrowRight,
PauseCircle,
CalendarDays,
FileSpreadsheet,
X,
AlertCircle,
History,
Archive,
ArrowUpRight,
LayoutGrid,
List,
Camera,
Table2,
Paperclip
} from 'lucide-react';
import { EditableField } from './EditableField';
const MONTHS = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
const CURRENT_YEAR = new Date().getFullYear();
const CURRENT_MONTH_IDX = new Date().getMonth();
function getDefaultMonthForNewItem(selectedYear: number): string {
if (selectedYear === CURRENT_YEAR && CURRENT_MONTH_IDX <= 2) return 'Январь';
return MONTHS[CURRENT_MONTH_IDX];
}
function isInspectionCandidate(el: InspectionMKDElement): boolean {
const st = [el.generalStatus, el.electroStatus, el.weldingStatus];
if (st.some(s => s === 'WARNING' || s === 'CRITICAL')) return true;
return !!el.repairType?.trim();
}
type InspectionCandidate = {
actId: string;
actNumber: string;
actDate: string;
sectionTitle: string;
element: InspectionMKDElement;
};
function collectCandidatesFromInspections(history: InspectionAct[]): InspectionCandidate[] {
const out: InspectionCandidate[] = [];
for (const act of history) {
const sections = act.sections || [];
const common = act.commonSections || [];
for (const s of sections) {
for (const el of s.elements || []) {
if (isInspectionCandidate(el)) out.push({ actId: act.id, actNumber: act.number, actDate: act.date, sectionTitle: s.title, element: el });
}
}
for (const s of common) {
for (const el of s.elements || []) {
if (isInspectionCandidate(el)) out.push({ actId: act.id, actNumber: act.number, actDate: act.date, sectionTitle: s.title, element: el });
}
}
}
return out;
}
type ViewMode = 'list' | 'months' | 'table';
export const BudgetPlanView: React.FC<{ building: Building, setBuilding: React.Dispatch<React.SetStateAction<Building>>, isEditing: boolean }> = ({ building, setBuilding, isEditing }) => {
const [showImportModal, setShowImportModal] = useState(false);
const [importData, setImportData] = useState('');
const [selectedYear, setSelectedYear] = useState(CURRENT_YEAR);
const [viewMode, setViewMode] = useState<ViewMode>('table');
const [showFromInspectionsModal, setShowFromInspectionsModal] = useState(false);
const [postponeModalId, setPostponeModalId] = useState<string | null>(null);
const [carryOverModalId, setCarryOverModalId] = useState<string | null>(null);
const [postponeReasonInput, setPostponeReasonInput] = useState('');
const [carryOverReasonInput, setCarryOverReasonInput] = useState('');
const [fromInspectionsYear, setFromInspectionsYear] = useState(selectedYear);
const [fromInspectionsMonth, setFromInspectionsMonth] = useState(getDefaultMonthForNewItem(selectedYear));
const [fromInspectionsSelectedActIds, setFromInspectionsSelectedActIds] = useState<string[]>([]);
const [fromInspectionsSelectedKeys, setFromInspectionsSelectedKeys] = useState<Record<string, boolean>>({});
const [invoicesForBuilding, setInvoicesForBuilding] = useState<PaymentInvoice[]>([]);
const [linkInvoicePlanItemId, setLinkInvoicePlanItemId] = useState<string | null>(null);
const [showCreatePlanItemModal, setShowCreatePlanItemModal] = useState(false);
const [createPlanItemMonth, setCreatePlanItemMonth] = useState<string | undefined>(undefined);
const [workCardItemId, setWorkCardItemId] = useState<string | null>(null);
type CreateTableRow = { workName: string; month: string; estimatedCost: number; byManagement: boolean };
const [createTableRows, setCreateTableRows] = useState<CreateTableRow[]>([]);
// Normalize existing data if years are missing
const plan = useMemo(() => {
return (building.annualPlan || []).map(item => ({
...item,
year: item.year || CURRENT_YEAR // Default to current if undefined
}));
}, [building.annualPlan]);
const availableYears = useMemo(() => {
const years = new Set([CURRENT_YEAR, CURRENT_YEAR + 1]);
plan.forEach(p => years.add(p.year));
return Array.from(years).sort((a, b) => b - a);
}, [plan]);
const filteredPlan = plan.filter(item => item.year === selectedYear);
const totalEstimated = filteredPlan.reduce((sum, item) => sum + (item.estimatedCost || 0), 0);
const totalActual = filteredPlan.reduce((sum, item) => sum + (item.actualCost || 0), 0);
const handleAddItem = (month?: string) => {
setCreatePlanItemMonth(month ?? getDefaultMonthForNewItem(selectedYear));
setShowCreatePlanItemModal(true);
};
const handleAddItemsFromTable = (rows: CreateTableRow[]) => {
const toAdd = rows.filter(r => (r.workName || '').trim());
if (toAdd.length === 0) return;
const baseTs = Date.now();
const newItems: PlanItem[] = toAdd.map((data, idx) => ({
id: `p-${baseTs}-${idx}`,
year: selectedYear,
month: data.month || getDefaultMonthForNewItem(selectedYear),
workName: (data.workName || '').trim() || 'Новая работа',
status: 'future' as const,
progress: 0,
estimatedCost: data.estimatedCost ?? 0,
byManagement: data.byManagement ?? false
}));
setBuilding(prev => ({
...prev,
annualPlan: [...(prev.annualPlan || []), ...newItems],
isDirty: true
}));
setShowCreatePlanItemModal(false);
setCreatePlanItemMonth(undefined);
setCreateTableRows([]);
};
const handleUpdateItem = (id: string, updates: Partial<PlanItem>) => {
setBuilding(prev => ({
...prev,
annualPlan: (prev.annualPlan || []).map(item => item.id === id ? { ...item, ...updates } : item),
isDirty: true
}));
};
const handleQuickComplete = (id: string) => {
const item = plan.find(p => p.id === id);
handleUpdateItem(id, {
progress: 100,
status: 'completed',
actualCost: item?.estimatedCost
});
};
const handlePostpone = (id: string, reason?: string) => {
const currentItem = plan.find(p => p.id === id);
if (!currentItem) return;
const currentMonthIdx = MONTHS.indexOf(currentItem.month);
if (currentMonthIdx === 11) return; // Dec -> use carry-over flow instead
const nextMonth = MONTHS[currentMonthIdx + 1];
handleUpdateItem(id, { month: nextMonth, status: 'future', progress: 0, postponeReason: reason || undefined });
};
const handleCarryOver = (id: string, targetYear: number, reason?: string) => {
const currentItem = plan.find(p => p.id === id);
if (!currentItem) return;
const carriedItem: PlanItem = {
...currentItem,
id: `p-carried-${Date.now()}`,
year: targetYear,
originalYear: currentItem.originalYear || currentItem.year,
status: 'future',
progress: 0,
month: 'Январь',
carryOverReason: reason || undefined
};
setBuilding(prev => {
const base = prev.annualPlan || [];
return {
...prev,
annualPlan: [
...base.map(item => item.id === id ? { ...item, status: 'carried_over' as const } : item),
carriedItem
],
isDirty: true
};
});
};
const handleDeleteItem = (id: string) => {
if (confirm('Удалить этот пункт плана?')) {
setBuilding(prev => ({
...prev,
annualPlan: (prev.annualPlan || []).filter(item => item.id !== id),
isDirty: true
}));
}
};
const handleAddFromInspections = () => {
const existingKeys = new Set((building.annualPlan || []).map(p => (p.sourceInspectionId && p.sourceElementId) ? `${p.sourceInspectionId}|${p.sourceElementId}` : null).filter(Boolean) as string[]);
const toAdd = inspectionCandidates.filter(c => {
const key = `${c.actId}|${c.element.id}`;
return fromInspectionsSelectedKeys[key] && !existingKeys.has(key);
});
const newItems: PlanItem[] = toAdd.map(c => {
const workName = [c.element.name, c.element.repairType?.trim() && `${c.element.repairType.trim()}`, c.sectionTitle && `, ${c.sectionTitle}`].filter(Boolean).join('');
return {
id: `p-ins-${Date.now()}-${c.element.id}`,
year: fromInspectionsYear,
month: fromInspectionsMonth,
workName,
status: 'future',
progress: 0,
estimatedCost: 0,
byManagement: false,
sourceInspectionId: c.actId,
sourceElementId: c.element.id,
sourceSectionTitle: c.sectionTitle
};
});
if (newItems.length === 0) return;
setBuilding(prev => ({
...prev,
annualPlan: [...(prev.annualPlan || []), ...newItems],
isDirty: true
}));
setShowFromInspectionsModal(false);
};
const parsePlanRow = (row: string, idx: number, prefix: string): PlanItem => {
const [name, month, cost] = row.split(';').map(s => s?.trim() ?? '');
const byManagement = /силами\s*ук|силами\s*у\.?к\.?/i.test(cost);
const estimatedCost = byManagement ? 0 : (Number(cost?.replace(/\s/g, '')) || 0);
return {
id: `${prefix}-${Date.now()}-${idx}`,
year: selectedYear,
workName: name || 'Без названия',
month: month || 'Январь',
estimatedCost,
byManagement,
status: 'future',
progress: 0
};
};
const handleImportCSV = () => {
const rows = importData.split('\n').filter(r => r.trim());
const newItems: PlanItem[] = rows.map((row, idx) => parsePlanRow(row, idx, 'p-imp'));
setBuilding(prev => ({
...prev,
annualPlan: [...(prev.annualPlan || []), ...newItems],
isDirty: true
}));
setShowImportModal(false);
setImportData('');
};
const handleLinkInvoice = async (invoiceId: number, planItemId: string) => {
try {
await apiClient.put(`/finance/payment-invoices/${invoiceId}`, {
planItemId,
planItemBuildingId: building.id
});
await fetchInvoices();
setLinkInvoicePlanItemId(null);
} catch (e) {
console.error('Link invoice failed:', e);
alert('Не удалось привязать счёт. Возможны только счета в статусе «Черновик» или «Отклонён».');
}
};
const isArchive = selectedYear < CURRENT_YEAR;
const overdueItems = useMemo(() => {
if (selectedYear !== CURRENT_YEAR || isArchive) return [];
const now = new Date().getMonth();
return filteredPlan.filter(p =>
p.status !== 'completed' && p.status !== 'carried_over' && MONTHS.indexOf(p.month) < now
);
}, [filteredPlan, selectedYear, isArchive]);
const inspectionCandidates = useMemo(() => collectCandidatesFromInspections(building.inspectionHistory || []), [building.inspectionHistory]);
const inspectionActsWithCandidates = useMemo(() => {
const actIds = new Set(inspectionCandidates.map(c => c.actId));
return (building.inspectionHistory || []).filter(a => actIds.has(a.id));
}, [building.inspectionHistory, inspectionCandidates]);
useEffect(() => {
if (showFromInspectionsModal) {
setFromInspectionsYear(selectedYear);
setFromInspectionsMonth(getDefaultMonthForNewItem(selectedYear));
setFromInspectionsSelectedActIds([]);
setFromInspectionsSelectedKeys({});
}
}, [showFromInspectionsModal, selectedYear]);
useEffect(() => {
if (showCreatePlanItemModal) {
const defaultMonth = createPlanItemMonth ?? getDefaultMonthForNewItem(selectedYear);
setCreateTableRows([{ workName: '', month: defaultMonth, estimatedCost: 0, byManagement: false }]);
}
}, [showCreatePlanItemModal, createPlanItemMonth, selectedYear]);
const fetchInvoices = useCallback(async () => {
try {
const res = await apiClient.get<{ invoices: PaymentInvoice[] }>(
`/finance/payment-invoices?buildingId=${encodeURIComponent(building.id)}&limit=500`
);
const list = (res as any)?.invoices ?? [];
setInvoicesForBuilding(Array.isArray(list) ? list : []);
} catch {
setInvoicesForBuilding([]);
}
}, [building.id]);
useEffect(() => {
fetchInvoices();
}, [fetchInvoices]);
const invoicesByPlanItemId = useMemo(() => {
const map: Record<string, PaymentInvoice[]> = {};
for (const inv of invoicesForBuilding) {
if (inv.planItemId && inv.planItemBuildingId === building.id) {
const k = inv.planItemId;
if (!map[k]) map[k] = [];
map[k].push(inv);
}
}
return map;
}, [invoicesForBuilding, building.id]);
return (
<div className="space-y-6 animate-fade-in">
{/* Year Selector Tabs */}
<div className="flex gap-1 bg-slate-200/50 p-1 rounded-xl overflow-x-auto no-scrollbar">
{availableYears.map(year => (
<button
key={year}
onClick={() => setSelectedYear(year)}
className={`flex-shrink-0 min-w-[5rem] px-4 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-1.5 whitespace-nowrap ${selectedYear === year ? 'bg-white text-primary-600 shadow-sm' : 'text-slate-500 hover:bg-white/50'}`}
>
{year < CURRENT_YEAR && <Archive className="w-3 h-3 opacity-50"/>}
{year} {year === CURRENT_YEAR && '(Тек.)'}
</button>
))}
</div>
{/* Budget Summary Card */}
<div className={`rounded-2xl p-6 text-white shadow-xl transition-colors duration-500 ${isArchive ? 'bg-gradient-to-br from-slate-600 to-slate-700' : 'bg-gradient-to-br from-slate-800 to-slate-900'}`}>
<div className="flex justify-between items-start mb-6">
<div>
<h3 className="text-slate-300 text-[10px] font-bold uppercase tracking-wider mb-1">
План работ на {selectedYear} год {isArchive && '(Архив)'}
</h3>
<p className="text-3xl font-black">{totalEstimated.toLocaleString()} </p>
</div>
<div className="p-3 bg-white/10 rounded-xl">
<PieChart className="w-6 h-6 text-primary-400"/>
</div>
</div>
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-white/10">
<div>
<span className="text-xs text-slate-300 block mb-1">Фактически исполнено</span>
<span className="text-lg font-bold">{totalActual.toLocaleString()} </span>
</div>
<div className="text-right">
<span className="text-xs text-slate-300 block mb-1">Процент выполнения</span>
<span className={`text-lg font-bold ${totalEstimated > 0 && (totalActual / totalEstimated) > 0.8 ? 'text-emerald-400' : 'text-amber-400'}`}>
{totalEstimated > 0 ? Math.round((totalActual / totalEstimated) * 100) : 0}%
</span>
</div>
</div>
</div>
{/* View toggle */}
{!isArchive && (
<div className="flex gap-1 bg-slate-200/50 p-1 rounded-xl w-fit flex-wrap">
<button
type="button"
onClick={() => setViewMode('table')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all ${viewMode === 'table' ? 'bg-white text-primary-600 shadow-sm' : 'text-slate-500 hover:bg-white/50'}`}
>
<Table2 className="w-3.5 h-3.5"/> Таблица
</button>
<button
type="button"
onClick={() => setViewMode('list')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all ${viewMode === 'list' ? 'bg-white text-primary-600 shadow-sm' : 'text-slate-500 hover:bg-white/50'}`}
>
<List className="w-3.5 h-3.5"/> Список
</button>
<button
type="button"
onClick={() => setViewMode('months')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all ${viewMode === 'months' ? 'bg-white text-primary-600 shadow-sm' : 'text-slate-500 hover:bg-white/50'}`}
>
<LayoutGrid className="w-3.5 h-3.5"/> По месяцам
</button>
</div>
)}
{/* Management Bar — Создать пункт открывает модалку во всех режимах */}
{!isArchive && (
<div className="flex flex-wrap gap-2">
<button
onClick={() => handleAddItem()}
className="flex-1 min-w-[120px] py-3 bg-primary-600 text-white rounded-xl font-bold text-xs flex items-center justify-center gap-2 hover:bg-primary-700 transition-all shadow-lg shadow-primary-500/20 active:scale-95"
>
<Plus className="w-4 h-4"/> Создать пункт
</button>
<button
onClick={() => setShowFromInspectionsModal(true)}
className="flex-1 min-w-[120px] py-3 bg-white border border-slate-200 text-slate-700 rounded-xl font-bold text-xs flex items-center justify-center gap-2 hover:bg-slate-50 transition-all active:scale-95"
>
<Camera className="w-4 h-4 text-primary-500"/> Пополнить из обходов
</button>
<button
onClick={() => setShowImportModal(true)}
className="flex-1 min-w-[120px] py-3 bg-white border border-slate-200 text-slate-700 rounded-xl font-bold text-xs flex items-center justify-center gap-2 hover:bg-slate-50 transition-all active:scale-95"
>
<Upload className="w-4 h-4 text-primary-500"/> Импорт
</button>
</div>
)}
{/* Empty plan hint */}
{filteredPlan.length === 0 && (
<div className="py-12 px-6 text-center bg-white rounded-2xl border border-dashed border-slate-300">
<CalendarDays className="w-10 h-10 mx-auto mb-3 text-slate-300"/>
<p className="text-sm font-medium text-slate-600 mb-4">План на {selectedYear} год не заполнен</p>
<p className="text-xs text-slate-500 mb-6">Нажмите «Создать пункт», пополните из обходов или импортируйте CSV</p>
{!isArchive && (
<div className="flex flex-wrap gap-2 justify-center">
<button type="button" onClick={() => handleAddItem()} className="py-2.5 px-4 bg-primary-600 text-white rounded-xl font-bold text-xs hover:bg-primary-700 transition-all flex items-center gap-1.5 mx-auto"><Plus className="w-4 h-4"/> Создать пункт</button>
<button type="button" onClick={() => setShowFromInspectionsModal(true)} className="py-2.5 px-4 bg-white border border-slate-200 text-slate-700 rounded-xl font-bold text-xs hover:bg-slate-50 transition-all"><Camera className="w-4 h-4 inline mr-1"/> Пополнить из обходов</button>
<button type="button" onClick={() => setShowImportModal(true)} className="py-2.5 px-4 bg-white border border-slate-200 text-slate-700 rounded-xl font-bold text-xs hover:bg-slate-50 transition-all"><Upload className="w-4 h-4 inline mr-1 text-primary-500"/> Импорт</button>
</div>
)}
</div>
)}
{/* Overdue block */}
{filteredPlan.length > 0 && overdueItems.length > 0 && (
<div className="rounded-xl border-2 border-red-200 bg-red-50/30 p-4 space-y-3">
<h3 className="text-xs font-black uppercase tracking-wider text-red-600 flex items-center gap-2">
<AlertCircle className="w-4 h-4"/> Просроченные ({overdueItems.length})
</h3>
<div className="space-y-2">
{overdueItems.map(item => (
<div key={item.id} className="flex flex-wrap items-center justify-between gap-2 py-2 px-3 bg-white rounded-lg border border-red-100">
<div>
<span className="font-bold text-slate-800 text-sm block">{item.workName}</span>
<span className="text-[10px] text-red-600 font-bold uppercase">{item.month}</span>
</div>
<div className="flex items-center gap-3">
<span className="text-sm font-bold text-slate-700">{item.byManagement ? 'силами УК' : `${item.estimatedCost?.toLocaleString() ?? 0}`}</span>
<span className="text-[9px] font-black uppercase px-2 py-0.5 rounded bg-red-100 text-red-700">Просрочено</span>
</div>
</div>
))}
</div>
</div>
)}
{/* Work Plan List (list view) or Months grid */}
{filteredPlan.length > 0 && viewMode === 'list' && (
<div className="space-y-4">
{[...filteredPlan].sort((a,b) => MONTHS.indexOf(a.month) - MONTHS.indexOf(b.month)).map((item) => {
const isOverdue = selectedYear === CURRENT_YEAR && item.status !== 'completed' && item.status !== 'carried_over' && MONTHS.indexOf(item.month) < new Date().getMonth();
const isCarryOver = !!item.originalYear && item.originalYear < item.year;
return (
<div
key={item.id}
role="button"
tabIndex={0}
onClick={() => setWorkCardItemId(item.id)}
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setWorkCardItemId(item.id); } }}
className={`bg-white p-4 rounded-xl border shadow-sm relative group transition-all cursor-pointer hover:border-primary-300 hover:shadow-md ${isOverdue ? 'border-red-200 bg-red-50/10' : 'border-slate-200'} ${isArchive ? 'opacity-90 grayscale-[0.3]' : ''} ${item.status === 'carried_over' ? 'opacity-60 bg-slate-50' : ''}`}
>
<div className="flex justify-between items-start mb-3">
<div className="flex items-center gap-2">
<div className={`p-1.5 rounded-lg ${item.status === 'completed' ? 'bg-emerald-50 text-emerald-600' : item.status === 'carried_over' ? 'bg-slate-200 text-slate-500' : 'bg-primary-50 text-primary-600'}`}>
{item.status === 'completed' ? <CheckCircle2 className="w-4 h-4"/> : item.status === 'carried_over' ? <ArrowUpRight className="w-4 h-4"/> : <Clock className="w-4 h-4"/>}
</div>
<div className="flex flex-col" onClick={e => e.stopPropagation()}>
<div className="flex items-center gap-2">
<EditableField
value={item.month}
onChange={(v) => handleUpdateItem(item.id, { month: v })}
isEditing={isEditing && !isArchive}
className={`text-[10px] font-black uppercase ${isOverdue ? 'text-red-500' : 'text-slate-400'}`}
/>
{isCarryOver && (
<span className="bg-amber-100 text-amber-700 text-[8px] px-1.5 py-0.5 rounded font-black uppercase flex items-center gap-0.5">
<History className="w-2 h-2"/> Перенос с {item.originalYear}
</span>
)}
</div>
{isOverdue && <span className="text-[8px] font-bold text-red-600 flex items-center gap-0.5"><AlertCircle className="w-2 h-2"/> ПРОСРОЧЕНО</span>}
{item.status === 'carried_over' && <span className="text-[8px] font-bold text-slate-500 uppercase">ПЕРЕНЕСЕНО НА {selectedYear + 1} Г.</span>}
{item.postponeReason && <span className="text-[10px] text-slate-500 mt-0.5 block">Причина переноса: {item.postponeReason}</span>}
{item.carryOverReason && isCarryOver && <span className="text-[10px] text-slate-500 mt-0.5 block">Причина переноса на год: {item.carryOverReason}</span>}
</div>
</div>
<div className="flex items-center gap-2 flex-wrap" onClick={e => e.stopPropagation()}>
{isEditing && !isArchive && (
<label className="flex items-center gap-1.5 text-[10px] font-bold text-slate-600 cursor-pointer">
<input type="checkbox" checked={!!item.byManagement} onChange={() => handleUpdateItem(item.id, { byManagement: !item.byManagement })} className="rounded accent-primary-600" />
силы УК
</label>
)}
{item.byManagement ? (
<span className="text-xs font-bold text-slate-600">силами УК</span>
) : (
<>
<Wallet className="w-3.5 h-3.5 text-slate-300"/>
<EditableField
value={item.estimatedCost}
onChange={(v) => handleUpdateItem(item.id, { estimatedCost: Number(v) })}
isEditing={isEditing && !isArchive}
type="number"
className="text-sm font-bold text-slate-800 text-right w-20"
/>
<span className="text-[10px] text-slate-400 font-bold"></span>
</>
)}
</div>
</div>
<div className="mb-4" onClick={e => e.stopPropagation()}>
<EditableField
value={item.workName}
onChange={(v) => handleUpdateItem(item.id, { workName: v })}
isEditing={isEditing && !isArchive}
className="font-bold text-slate-700 text-sm w-full block"
/>
</div>
{/* Progress Bar and Actions */}
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
<div className="flex-1 space-y-1" onClick={e => e.stopPropagation()}>
<div className="flex justify-between items-end">
<span className="text-[9px] text-slate-400 font-bold uppercase tracking-tighter">Готовность объекта</span>
<span className="text-[10px] font-black text-slate-700">{item.progress}%</span>
</div>
<div className="w-full h-2 bg-slate-100 rounded-full overflow-hidden">
{isEditing && !isArchive ? (
<input
type="range" min="0" max="100"
value={item.progress}
onChange={(e) => handleUpdateItem(item.id, { progress: Number(e.target.value), status: Number(e.target.value) === 100 ? 'completed' : 'current' })}
className="w-full h-full accent-primary-600 cursor-pointer"
/>
) : (
<div
className={`h-full rounded-full transition-all duration-500 ${item.status === 'completed' ? 'bg-emerald-500' : 'bg-primary-500'}`}
style={{ width: `${item.progress}%` }}
/>
)}
</div>
</div>
{/* Quick Actions */}
{!isEditing && !isArchive && item.status !== 'completed' && item.status !== 'carried_over' && (
<div className="flex gap-1" onClick={e => e.stopPropagation()}>
<button
type="button"
onClick={() => handleQuickComplete(item.id)}
className="p-2 bg-emerald-50 text-emerald-600 rounded-lg hover:bg-emerald-100 transition-colors"
title="Выполнено"
>
<CheckCircle2 className="w-4 h-4"/>
</button>
<button
type="button"
onClick={() => { setCarryOverReasonInput(''); setCarryOverModalId(item.id); }}
className="p-2 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition-colors"
title={`Перенести на ${selectedYear + 1} год`}
>
<ArrowUpRight className="w-4 h-4"/>
</button>
<button
type="button"
onClick={() => {
if (MONTHS.indexOf(item.month) === 11) {
setCarryOverReasonInput(''); setCarryOverModalId(item.id);
} else {
setPostponeReasonInput(''); setPostponeModalId(item.id);
}
}}
className="p-2 bg-amber-50 text-amber-600 rounded-lg hover:bg-amber-100 transition-colors"
title="Перенести на след. месяц"
>
<ArrowRight className="w-4 h-4"/>
</button>
<button
type="button"
onClick={() => handleUpdateItem(item.id, { progress: 0, status: 'future' })}
className="p-2 bg-slate-100 text-slate-500 rounded-lg hover:bg-slate-200 transition-colors"
title="Отложить"
>
<PauseCircle className="w-4 h-4"/>
</button>
</div>
)}
{isEditing && !isArchive && (
<button
type="button"
onClick={e => { e.stopPropagation(); handleDeleteItem(item.id); }}
className="p-2 text-red-300 hover:text-red-500 transition-colors"
>
<Trash2 className="w-5 h-5"/>
</button>
)}
</div>
</div>
);
})}
</div>
)}
{/* Months grid view */}
{filteredPlan.length > 0 && viewMode === 'months' && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{MONTHS.map(month => {
const monthItems = filteredPlan.filter(p => p.month === month);
const monthTotal = monthItems.reduce((s, p) => s + (p.byManagement ? 0 : (p.estimatedCost || 0)), 0);
return (
<div key={month} className="bg-white rounded-xl border border-slate-200 shadow-sm p-4 flex flex-col">
<div className="flex justify-between items-center mb-3">
<span className="text-[10px] font-black uppercase text-slate-500">{month}</span>
<span className="text-xs font-bold text-slate-700">{monthItems.length} работ · {monthTotal.toLocaleString()} </span>
</div>
<div className="space-y-2 flex-1 min-h-0 overflow-y-auto max-h-48">
{monthItems.map(item => (
<div
key={item.id}
role="button"
tabIndex={0}
onClick={() => setWorkCardItemId(item.id)}
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setWorkCardItemId(item.id); } }}
className="py-1.5 border-b border-slate-100 last:border-0 cursor-pointer hover:bg-slate-50 rounded px-1 -mx-1 transition-colors"
>
<div className="flex justify-between items-center gap-2">
<span className="text-xs font-medium text-slate-700 truncate flex-1">{item.workName}</span>
<span className="text-[10px] font-bold text-slate-500 shrink-0">{item.byManagement ? 'силами УК' : `${item.estimatedCost?.toLocaleString() ?? 0}`}</span>
</div>
<div className="flex items-center gap-1.5 mt-0.5">
<div className="flex-1 h-1 bg-slate-100 rounded-full overflow-hidden">
<div className={`h-full rounded-full ${item.status === 'completed' ? 'bg-emerald-500' : 'bg-primary-500'}`} style={{ width: `${item.progress}%` }} />
</div>
<span className="text-[9px] font-bold text-slate-400 w-6">{item.progress}%</span>
</div>
</div>
))}
</div>
{!isArchive && (
<button
type="button"
onClick={() => handleAddItem(month)}
className="mt-3 w-full py-2 text-[10px] font-bold uppercase tracking-wider text-primary-600 border border-dashed border-primary-200 rounded-lg hover:bg-primary-50 transition-colors flex items-center justify-center gap-1"
>
<Plus className="w-3 h-3"/> Добавить в {month}
</button>
)}
</div>
);
})}
</div>
)}
{/* Table — только просмотр; клик по строке открывает карточку работы; добавление через модалку */}
{viewMode === 'table' && !isArchive && (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="p-4 border-b border-slate-200 bg-slate-50/50">
<h3 className="text-sm font-bold text-slate-800">План работ на {selectedYear} год</h3>
<p className="text-xs text-slate-500 mt-0.5">Нажмите на строку, чтобы открыть карточку работы. Новый пункт кнопка «Создать пункт» или «Добавить строку» ниже.</p>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-50 border-b border-slate-200 sticky top-0">
<tr>
<th className="p-3 text-left text-xs font-bold text-slate-600 uppercase">Работа</th>
<th className="p-3 text-left text-xs font-bold text-slate-600 uppercase">Бюджет / силы УК</th>
<th className="p-3 text-left text-xs font-bold text-slate-600 uppercase">Месяц</th>
<th className="p-3 text-left text-xs font-bold text-slate-600 uppercase">План / Факт</th>
<th className="p-3 text-center text-xs font-bold text-slate-600 uppercase w-12">Статус</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{[...filteredPlan].sort((a, b) => MONTHS.indexOf(a.month) - MONTHS.indexOf(b.month)).map((item) => {
const invs = invoicesByPlanItemId[item.id] ?? [];
const fact = (invs.reduce((s, i) => s + (i.totalAmount ?? 0), 0) || item.actualCost) ?? 0;
const planLabel = item.byManagement ? 'силами УК' : (item.estimatedCost?.toLocaleString() ?? 0) + ' ₽';
return (
<tr
key={item.id}
onClick={() => setWorkCardItemId(item.id)}
className="hover:bg-primary-50/50 cursor-pointer transition-colors"
>
<td className="p-3 font-medium text-slate-800">{item.workName}</td>
<td className="p-3 text-slate-700">{planLabel}</td>
<td className="p-3 text-[10px] font-bold uppercase text-slate-500">{item.month}</td>
<td className="p-3">
<div className="flex flex-col gap-0.5">
<span className="text-xs text-slate-600">План: {planLabel}</span>
<span className="text-xs font-bold text-slate-800">Факт: {fact > 0 ? `${fact.toLocaleString()}` : '—'}{invs.length > 0 && <span className="text-[10px] text-slate-400 ml-1">({invs.length} сч.)</span>}</span>
</div>
</td>
<td className="p-3 text-center">
{item.status === 'completed' && <CheckCircle2 className="w-5 h-5 text-emerald-500 mx-auto"/>}
{item.status === 'carried_over' && <ArrowUpRight className="w-5 h-5 text-slate-400 mx-auto"/>}
{(item.status === 'future' || item.status === 'current') && <span className="text-[10px] font-bold text-slate-400">{item.progress}%</span>}
</td>
</tr>
);
})}
</tbody>
<tfoot className="bg-slate-50 border-t-2 border-slate-200">
<tr>
<td colSpan={5} className="p-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<span className="text-xs text-slate-500">Всего позиций: {filteredPlan.length}</span>
<button
type="button"
onClick={(e) => { e.stopPropagation(); handleAddItem(); }}
className="px-4 py-2 bg-primary-100 text-primary-700 rounded-lg text-sm font-bold hover:bg-primary-200 transition-colors flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Добавить строку
</button>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
)}
{viewMode === 'table' && isArchive && filteredPlan.length > 0 && (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead><tr className="bg-slate-50 border-b border-slate-200">
<th className="px-4 py-3 text-[10px] font-black uppercase text-slate-500">Работа</th>
<th className="px-4 py-3 text-[10px] font-black uppercase text-slate-500">Бюджет / силы УК</th>
<th className="px-4 py-3 text-[10px] font-black uppercase text-slate-500">Месяц</th>
<th className="px-4 py-3 text-[10px] font-black uppercase text-slate-500">План / Факт</th>
</tr></thead>
<tbody>
{[...filteredPlan].sort((a, b) => MONTHS.indexOf(a.month) - MONTHS.indexOf(b.month)).map((item) => {
const invs = invoicesByPlanItemId[item.id] ?? [];
const fact = (invs.reduce((s, i) => s + (i.totalAmount ?? 0), 0) || item.actualCost) ?? 0;
const planLabel = item.byManagement ? 'силами УК' : (item.estimatedCost?.toLocaleString() ?? 0) + ' ₽';
return (
<tr key={item.id} className="border-b border-slate-100">
<td className="px-4 py-3 font-medium text-slate-800 text-sm">{item.workName}</td>
<td className="px-4 py-3 text-sm font-bold text-slate-700">{planLabel}</td>
<td className="px-4 py-3 text-[10px] font-bold uppercase text-slate-500">{item.month}</td>
<td className="px-4 py-3 text-xs">План: {planLabel} · Факт: {fact > 0 ? `${fact.toLocaleString()}` : '—'}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
{/* Модальное окно создания пунктов плана — табличная часть (сразу много строк) */}
{showCreatePlanItemModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in" onClick={() => setShowCreatePlanItemModal(false)}>
<div className="bg-white rounded-2xl w-full max-w-3xl max-h-[90vh] shadow-2xl animate-slide-up overflow-hidden flex flex-col" onClick={e => e.stopPropagation()}>
<div className="p-6 border-b border-slate-200 flex-shrink-0">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<div className="p-2 bg-primary-50 text-primary-600 rounded-lg"><Plus className="w-5 h-5"/></div>
<h3 className="text-lg font-bold text-slate-800">Создать пункты плана</h3>
</div>
<button type="button" onClick={() => setShowCreatePlanItemModal(false)} className="p-2 hover:bg-slate-100 rounded-full"><X className="w-5 h-5 text-slate-400"/></button>
</div>
<p className="text-xs text-slate-500 mt-2">Год: {selectedYear}. Добавляйте строки, заполняйте таблицу и нажмите «Добавить в план» все заполненные строки станут пунктами плана.</p>
</div>
<div className="flex-1 overflow-auto p-6">
<table className="w-full text-sm border border-slate-200 rounded-xl overflow-hidden">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="p-2 text-left text-xs font-bold text-slate-600 uppercase">Работа</th>
<th className="p-2 text-left text-xs font-bold text-slate-600 uppercase w-28">Месяц</th>
<th className="p-2 text-left text-xs font-bold text-slate-600 uppercase">Бюджет / силы УК</th>
<th className="p-2 w-10"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{createTableRows.map((row, idx) => (
<tr key={idx} className="hover:bg-slate-50/50">
<td className="p-2">
<input
type="text"
value={row.workName}
onChange={e => setCreateTableRows(prev => prev.map((r, i) => i === idx ? { ...r, workName: e.target.value } : r))}
className="w-full px-2 py-1.5 border border-slate-300 rounded text-sm focus:ring-2 focus:ring-primary-500 outline-none"
placeholder="Название работы"
/>
</td>
<td className="p-2">
<select
value={row.month}
onChange={e => setCreateTableRows(prev => prev.map((r, i) => i === idx ? { ...r, month: e.target.value } : r))}
className="w-full px-2 py-1.5 border border-slate-300 rounded text-xs focus:ring-2 focus:ring-primary-500 outline-none"
>
{MONTHS.map(m => <option key={m} value={m}>{m}</option>)}
</select>
</td>
<td className="p-2">
<div className="flex items-center gap-2 flex-wrap">
<label className="flex items-center gap-1.5 cursor-pointer shrink-0">
<input
type="checkbox"
checked={row.byManagement}
onChange={e => setCreateTableRows(prev => prev.map((r, i) => i === idx ? { ...r, byManagement: e.target.checked } : r))}
className="rounded accent-primary-600"
/>
<span className="text-xs font-medium text-slate-700">силы УК</span>
</label>
{!row.byManagement && (
<>
<input
type="number"
value={row.estimatedCost || ''}
onChange={e => setCreateTableRows(prev => prev.map((r, i) => i === idx ? { ...r, estimatedCost: Number(e.target.value) || 0 } : r))}
className="w-24 px-2 py-1.5 border border-slate-300 rounded text-sm text-right focus:ring-2 focus:ring-primary-500 outline-none"
placeholder="0"
min={0}
/>
<span className="text-[10px] text-slate-500"></span>
</>
)}
</div>
</td>
<td className="p-2">
<button
type="button"
onClick={() => setCreateTableRows(prev => prev.length > 1 ? prev.filter((_, i) => i !== idx) : prev)}
className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded"
title="Удалить строку"
>
<Trash2 className="w-4 h-4"/>
</button>
</td>
</tr>
))}
</tbody>
<tfoot className="bg-slate-50 border-t-2 border-slate-200">
<tr>
<td colSpan={4} className="p-3">
<button
type="button"
onClick={() => setCreateTableRows(prev => [...prev, { workName: '', month: createPlanItemMonth ?? getDefaultMonthForNewItem(selectedYear), estimatedCost: 0, byManagement: false }])}
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg text-sm font-bold hover:bg-slate-200 transition-colors flex items-center gap-2"
>
<Plus className="w-4 h-4"/> Добавить строку
</button>
</td>
</tr>
</tfoot>
</table>
<div className="flex gap-3 mt-6">
<button type="button" onClick={() => setShowCreatePlanItemModal(false)} className="flex-1 py-2.5 text-slate-600 font-bold bg-slate-100 rounded-xl hover:bg-slate-200">Отмена</button>
<button
type="button"
onClick={() => handleAddItemsFromTable(createTableRows)}
disabled={!createTableRows.some(r => (r.workName || '').trim())}
className="flex-1 py-2.5 text-white font-bold bg-primary-600 rounded-xl hover:bg-primary-700 disabled:opacity-50 flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4"/> Добавить в план ({createTableRows.filter(r => (r.workName || '').trim()).length} шт.)
</button>
</div>
</div>
</div>
</div>
)}
{/* Карточка работы */}
{workCardItemId && (() => {
const item = plan.find(p => p.id === workCardItemId);
if (!item) return null;
const invs = invoicesByPlanItemId[item.id] ?? [];
const fact = (invs.reduce((s, i) => s + (i.totalAmount ?? 0), 0) || item.actualCost) ?? 0;
const planLabel = item.byManagement ? 'силами УК' : (item.estimatedCost?.toLocaleString() ?? 0) + ' ₽';
const isOverdue = selectedYear === CURRENT_YEAR && item.status !== 'completed' && item.status !== 'carried_over' && MONTHS.indexOf(item.month) < new Date().getMonth();
const isCarryOver = !!item.originalYear && item.originalYear < item.year;
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in" onClick={() => setWorkCardItemId(null)}>
<div className="bg-white rounded-2xl w-full max-w-lg 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-shrink-0">
<div className="flex justify-between items-start">
<div className="flex items-center gap-3">
<div className={`p-2.5 rounded-xl ${item.status === 'completed' ? 'bg-emerald-50 text-emerald-600' : item.status === 'carried_over' ? 'bg-slate-100 text-slate-500' : 'bg-primary-50 text-primary-600'}`}>
{item.status === 'completed' ? <CheckCircle2 className="w-6 h-6"/> : item.status === 'carried_over' ? <ArrowUpRight className="w-6 h-6"/> : <Clock className="w-6 h-6"/>}
</div>
<div>
<h3 className="text-lg font-bold text-slate-800">{item.workName}</h3>
<p className="text-xs text-slate-500 mt-0.5">{item.month} · {selectedYear} г.</p>
{isCarryOver && item.originalYear && <span className="text-[10px] text-amber-600 bg-amber-50 px-1.5 py-0.5 rounded font-bold">Перенос с {item.originalYear} г.</span>}
{isOverdue && <span className="text-[10px] text-red-600 bg-red-50 px-1.5 py-0.5 rounded font-bold block mt-1">Просрочено</span>}
</div>
</div>
<button type="button" onClick={() => setWorkCardItemId(null)} className="p-2 hover:bg-slate-100 rounded-full"><X className="w-5 h-5 text-slate-400"/></button>
</div>
</div>
<div className="p-6 flex-1 overflow-y-auto space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="p-3 bg-slate-50 rounded-xl">
<span className="text-[10px] font-bold uppercase text-slate-500 block mb-1">План</span>
<span className="text-sm font-bold text-slate-800">{planLabel}</span>
</div>
<div className="p-3 bg-slate-50 rounded-xl">
<span className="text-[10px] font-bold uppercase text-slate-500 block mb-1">Факт</span>
<span className="text-sm font-bold text-slate-800">{fact > 0 ? `${fact.toLocaleString()}` : '—'}</span>
{invs.length > 0 && <span className="text-[10px] text-slate-400 block">({invs.length} сч.)</span>}
</div>
</div>
<div>
<span className="text-[10px] font-bold uppercase text-slate-500 block mb-2">Готовность</span>
<div className="flex items-center gap-3">
<div className="flex-1 h-3 bg-slate-100 rounded-full overflow-hidden">
<div className={`h-full rounded-full ${item.status === 'completed' ? 'bg-emerald-500' : 'bg-primary-500'}`} style={{ width: `${item.progress}%` }} />
</div>
<span className="text-sm font-bold text-slate-700 w-10">{item.progress}%</span>
</div>
</div>
{(item.postponeReason || (item.carryOverReason && isCarryOver)) && (
<div className="p-3 bg-amber-50 rounded-xl text-sm text-slate-700">
{item.postponeReason && <p><span className="font-bold text-amber-800">Причина переноса (месяц):</span> {item.postponeReason}</p>}
{item.carryOverReason && isCarryOver && <p className="mt-1"><span className="font-bold text-amber-800">Причина переноса (год):</span> {item.carryOverReason}</p>}
</div>
)}
{!isArchive && (
<div className="flex flex-wrap gap-2 pt-2">
<button type="button" onClick={() => setLinkInvoicePlanItemId(item.id)} className="px-3 py-2 bg-slate-100 text-slate-700 rounded-lg text-xs font-bold flex items-center gap-1.5 hover:bg-slate-200"><Paperclip className="w-4 h-4"/> Привязать счёт</button>
{item.status !== 'completed' && item.status !== 'carried_over' && (
<>
<button type="button" onClick={() => { handleQuickComplete(item.id); setWorkCardItemId(null); }} className="px-3 py-2 bg-emerald-100 text-emerald-700 rounded-lg text-xs font-bold flex items-center gap-1.5 hover:bg-emerald-200"><CheckCircle2 className="w-4 h-4"/> Выполнено</button>
<button type="button" onClick={() => { setCarryOverReasonInput(''); setCarryOverModalId(item.id); setWorkCardItemId(null); }} className="px-3 py-2 bg-blue-100 text-blue-700 rounded-lg text-xs font-bold flex items-center gap-1.5 hover:bg-blue-200"><ArrowUpRight className="w-4 h-4"/> На след. год</button>
<button type="button" onClick={() => { setPostponeReasonInput(''); setPostponeModalId(item.id); setWorkCardItemId(null); }} className="px-3 py-2 bg-amber-100 text-amber-700 rounded-lg text-xs font-bold flex items-center gap-1.5 hover:bg-amber-200"><ArrowRight className="w-4 h-4"/> На след. месяц</button>
<button type="button" onClick={() => { handleUpdateItem(item.id, { progress: 0, status: 'future' }); }} className="px-3 py-2 bg-slate-100 text-slate-600 rounded-lg text-xs font-bold hover:bg-slate-200"><PauseCircle className="w-4 h-4"/> Отложить</button>
</>
)}
<button type="button" onClick={() => { if (confirm('Удалить этот пункт плана?')) { handleDeleteItem(item.id); setWorkCardItemId(null); } }} className="px-3 py-2 bg-red-50 text-red-600 rounded-lg text-xs font-bold hover:bg-red-100 flex items-center gap-1.5"><Trash2 className="w-4 h-4"/> Удалить</button>
</div>
)}
</div>
</div>
</div>
);
})()}
{/* Link invoice to plan item modal */}
{linkInvoicePlanItemId && (() => {
const it = filteredPlan.find(p => p.id === linkInvoicePlanItemId);
const unlinked = invoicesForBuilding.filter(inv => !inv.planItemId);
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in" onClick={() => setLinkInvoicePlanItemId(null)}>
<div className="bg-white rounded-2xl w-full max-w-lg p-6 shadow-2xl animate-slide-up max-h-[80vh] overflow-hidden flex flex-col" onClick={e => e.stopPropagation()}>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center gap-2">
<Paperclip className="w-5 h-5 text-primary-600"/>
<h3 className="text-lg font-bold text-slate-800">Привязать счёт к работе</h3>
</div>
<button onClick={() => setLinkInvoicePlanItemId(null)} className="p-2 hover:bg-slate-100 rounded-full"><X className="w-5 h-5 text-slate-400"/></button>
</div>
{it && <p className="text-sm text-slate-600 mb-4">«{it.workName}»</p>}
<div className="flex-1 overflow-y-auto min-h-0">
{unlinked.length === 0 ? (
<p className="text-sm text-slate-500 py-4">Нет непривязанных счетов по этому дому. Создайте счёт в разделе Финансы и отметьте «Из плана работ».</p>
) : (
<div className="space-y-2">
{unlinked.map(inv => (
<button
key={inv.id}
type="button"
onClick={() => handleLinkInvoice(inv.id, linkInvoicePlanItemId!)}
className="w-full text-left p-3 rounded-xl border border-slate-200 hover:border-primary-300 hover:bg-primary-50/50 transition-colors"
>
<span className="font-medium text-slate-800 block">{inv.invoiceNumber} · {inv.contractorName}</span>
<span className="text-xs text-slate-500">{inv.totalAmount?.toLocaleString()} </span>
</button>
))}
</div>
)}
</div>
</div>
</div>
);
})()}
{/* Postpone (next month) modal */}
{postponeModalId && (() => {
const it = filteredPlan.find(p => p.id === postponeModalId);
if (!it) return null;
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in" onClick={() => { setPostponeModalId(null); setPostponeReasonInput(''); }}>
<div className="bg-white rounded-2xl w-full max-w-md 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-600 mb-4">«{it.workName}»</p>
<label className="block text-xs font-bold text-slate-500 uppercase mb-2">Причина переноса</label>
<textarea
value={postponeReasonInput}
onChange={e => setPostponeReasonInput(e.target.value)}
placeholder="Укажите причину (необязательно)"
className="w-full h-24 p-3 bg-slate-50 border border-slate-200 rounded-xl text-sm resize-none focus:ring-2 focus:ring-primary-500 outline-none"
/>
<div className="flex gap-3 mt-6">
<button onClick={() => { setPostponeModalId(null); setPostponeReasonInput(''); }} className="flex-1 py-2.5 text-slate-600 font-bold bg-slate-100 rounded-xl hover:bg-slate-200">Отмена</button>
<button onClick={() => { handlePostpone(postponeModalId, postponeReasonInput || undefined); setPostponeModalId(null); setPostponeReasonInput(''); }} className="flex-1 py-2.5 text-white font-bold bg-primary-600 rounded-xl hover:bg-primary-700">Перенести</button>
</div>
</div>
</div>
);
})()}
{/* Carry-over (next year) modal */}
{carryOverModalId && (() => {
const it = filteredPlan.find(p => p.id === carryOverModalId);
if (!it) return null;
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in" onClick={() => { setCarryOverModalId(null); setCarryOverReasonInput(''); }}>
<div className="bg-white rounded-2xl w-full max-w-md p-6 shadow-2xl animate-slide-up" onClick={e => e.stopPropagation()}>
<h3 className="text-lg font-bold text-slate-800 mb-2">Перенести на {selectedYear + 1} год</h3>
<p className="text-sm text-slate-600 mb-4">«{it.workName}»</p>
<label className="block text-xs font-bold text-slate-500 uppercase mb-2">Причина переноса на следующий год</label>
<textarea
value={carryOverReasonInput}
onChange={e => setCarryOverReasonInput(e.target.value)}
placeholder="Укажите причину (необязательно)"
className="w-full h-24 p-3 bg-slate-50 border border-slate-200 rounded-xl text-sm resize-none focus:ring-2 focus:ring-primary-500 outline-none"
/>
<div className="flex gap-3 mt-6">
<button onClick={() => { setCarryOverModalId(null); setCarryOverReasonInput(''); }} className="flex-1 py-2.5 text-slate-600 font-bold bg-slate-100 rounded-xl hover:bg-slate-200">Отмена</button>
<button onClick={() => { handleCarryOver(carryOverModalId, selectedYear + 1, carryOverReasonInput || undefined); setCarryOverModalId(null); setCarryOverReasonInput(''); }} className="flex-1 py-2.5 text-white font-bold bg-primary-600 rounded-xl hover:bg-primary-700">Перенести</button>
</div>
</div>
</div>
);
})()}
{/* Пополнить из обходов modal */}
{showFromInspectionsModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in" onClick={() => setShowFromInspectionsModal(false)}>
<div className="bg-white rounded-2xl w-full max-w-2xl 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-shrink-0">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<div className="p-2 bg-primary-50 text-primary-600 rounded-lg"><Camera className="w-5 h-5"/></div>
<h3 className="text-lg font-bold text-slate-800">Пополнить план из обходов</h3>
</div>
<button onClick={() => setShowFromInspectionsModal(false)} className="p-2 hover:bg-slate-100 rounded-full"><X className="w-5 h-5 text-slate-400"/></button>
</div>
{inspectionActsWithCandidates.length === 0 ? (
<p className="text-sm text-slate-500 mt-4">Нет осмотров с замечаниями. Проведите обход с фотофиксацией и отметьте элементы с ремонтом.</p>
) : (
<>
<div className="flex flex-wrap gap-4 mt-4">
<div>
<label className="block text-[10px] font-bold uppercase text-slate-500 mb-1">Год</label>
<select value={fromInspectionsYear} onChange={e => setFromInspectionsYear(Number(e.target.value))} className="px-3 py-2 border border-slate-200 rounded-lg text-sm font-medium">
{availableYears.map(y => <option key={y} value={y}>{y}</option>)}
</select>
</div>
<div>
<label className="block text-[10px] font-bold uppercase text-slate-500 mb-1">Месяц</label>
<select value={fromInspectionsMonth} onChange={e => setFromInspectionsMonth(e.target.value)} className="px-3 py-2 border border-slate-200 rounded-lg text-sm font-medium">
{MONTHS.map(m => <option key={m} value={m}>{m}</option>)}
</select>
</div>
</div>
<p className="text-xs text-slate-500 mt-2">Выберите акты осмотра, затем отметьте замечания для добавления в план.</p>
</>
)}
</div>
{inspectionActsWithCandidates.length > 0 && (
<>
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{inspectionActsWithCandidates.map(act => {
const count = inspectionCandidates.filter(c => c.actId === act.id).length;
const checked = fromInspectionsSelectedActIds.includes(act.id);
return (
<div key={act.id} className="border border-slate-200 rounded-xl overflow-hidden">
<label className="flex items-center gap-3 p-3 bg-slate-50 cursor-pointer hover:bg-slate-100">
<input type="checkbox" checked={checked} onChange={() => setFromInspectionsSelectedActIds(prev => checked ? prev.filter(id => id !== act.id) : [...prev, act.id])} className="rounded accent-primary-600" />
<span className="font-bold text-slate-800">Акт {act.number}</span>
<span className="text-slate-500 text-sm">{act.date}</span>
<span className="text-xs font-bold text-amber-600 bg-amber-50 px-2 py-0.5 rounded">{count} замечаний</span>
</label>
{checked && (
<div className="p-3 pt-0 space-y-2">
{inspectionCandidates.filter(c => c.actId === act.id).map(c => {
const key = `${c.actId}|${c.element.id}`;
const sel = !!fromInspectionsSelectedKeys[key];
return (
<label key={key} className="flex items-start gap-3 p-2 rounded-lg hover:bg-slate-50 cursor-pointer">
<input type="checkbox" checked={sel} onChange={() => setFromInspectionsSelectedKeys(prev => ({ ...prev, [key]: !prev[key] }))} className="mt-0.5 rounded accent-primary-600 shrink-0" />
<div className="min-w-0 flex-1">
<span className="font-medium text-slate-800 block">{c.element.name}</span>
<div className="flex flex-wrap gap-2 mt-0.5">
{c.sectionTitle && <span className="text-[10px] text-slate-500">{c.sectionTitle}</span>}
{c.element.repairType?.trim() && <span className="text-[10px] text-amber-600 bg-amber-50 px-1.5 py-0.5 rounded">{c.element.repairType}</span>}
</div>
</div>
</label>
);
})}
</div>
)}
</div>
);
})}
</div>
<div className="p-6 border-t border-slate-200 flex-shrink-0 flex gap-3">
<button onClick={() => setShowFromInspectionsModal(false)} className="flex-1 py-2.5 text-slate-600 font-bold bg-slate-100 rounded-xl hover:bg-slate-200">Отмена</button>
<button
onClick={handleAddFromInspections}
disabled={Object.keys(fromInspectionsSelectedKeys).filter(k => fromInspectionsSelectedKeys[k]).length === 0}
className="flex-1 py-2.5 text-white font-bold bg-primary-600 rounded-xl hover:bg-primary-700 disabled:opacity-50 flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4"/> Добавить выбранные в план
</button>
</div>
</>
)}
</div>
</div>
)}
{/* Import Modal */}
{showImportModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in" onClick={() => setShowImportModal(false)}>
<div className="bg-white rounded-2xl w-full max-w-xl p-6 shadow-2xl animate-slide-up" onClick={e => e.stopPropagation()}>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center gap-2">
<div className="p-2 bg-primary-50 text-primary-600 rounded-lg"><FileSpreadsheet className="w-5 h-5"/></div>
<h3 className="text-lg font-bold text-slate-800">Импорт плана в {selectedYear} год</h3>
</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>
<textarea
value={importData}
onChange={(e) => setImportData(e.target.value)}
placeholder="Работа;Месяц;Сумма (или силы УК)"
className="w-full h-48 p-4 bg-slate-50 border border-slate-200 rounded-xl text-sm font-mono focus:ring-2 focus:ring-primary-500 outline-none resize-none"
/>
<div className="mt-6 flex gap-3">
<button onClick={() => setShowImportModal(false)} className="flex-1 py-3 text-slate-600 font-bold bg-slate-100 rounded-xl hover:bg-slate-200">Отмена</button>
<button
onClick={handleImportCSV}
disabled={!importData.trim()}
className="flex-1 py-3 text-white font-bold bg-primary-600 rounded-xl hover:bg-primary-700 disabled:opacity-50 flex items-center justify-center gap-2"
>
<Upload className="w-4 h-4"/> Загрузить в {selectedYear} год
</button>
</div>
</div>
</div>
)}
</div>
);
};