Files
mkd/components/building/BudgetPlanView.tsx

1184 lines
82 KiB
TypeScript
Raw Permalink Normal View History

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