796 lines
49 KiB
TypeScript
796 lines
49 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
|||
|
|
import { InspectionSection, InspectionCommonSection, InspectionMKDElement, Building } from '../../types';
|
|||
|
|
import {
|
|||
|
|
FLOOR_ELEMENT_NAMES,
|
|||
|
|
ENTRANCE_ELEMENT_NAMES,
|
|||
|
|
COMMON_SECTION_ELEMENTS,
|
|||
|
|
buildElementsFromNames
|
|||
|
|
} from '../../inspectionElementTemplates';
|
|||
|
|
import {
|
|||
|
|
ChevronDown,
|
|||
|
|
ChevronUp,
|
|||
|
|
Plus,
|
|||
|
|
X,
|
|||
|
|
Trash2,
|
|||
|
|
Camera,
|
|||
|
|
Image as ImageIcon,
|
|||
|
|
Gauge,
|
|||
|
|
DoorClosed,
|
|||
|
|
Building2,
|
|||
|
|
Save,
|
|||
|
|
Layers,
|
|||
|
|
Paperclip,
|
|||
|
|
ListPlus
|
|||
|
|
} from 'lucide-react';
|
|||
|
|
|
|||
|
|
interface DynamicInspectionSectionsProps {
|
|||
|
|
sections: InspectionSection[];
|
|||
|
|
commonSections: InspectionCommonSection[];
|
|||
|
|
building: Building;
|
|||
|
|
onUpdateSections: (sections: InspectionSection[]) => void;
|
|||
|
|
onUpdateCommonSections: (commonSections: InspectionCommonSection[]) => void;
|
|||
|
|
onSaveToBuilding?: (entranceData: any, floorData: any) => void; // Сохранение данных в building
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const DynamicInspectionSections: React.FC<DynamicInspectionSectionsProps> = ({
|
|||
|
|
sections,
|
|||
|
|
commonSections,
|
|||
|
|
building,
|
|||
|
|
onUpdateSections,
|
|||
|
|
onUpdateCommonSections,
|
|||
|
|
onSaveToBuilding
|
|||
|
|
}) => {
|
|||
|
|
const [activeTab, setActiveTab] = useState<string | null>(sections.length > 0 ? sections[0].id : null);
|
|||
|
|
const [editingElement, setEditingElement] = useState<{ sectionId: string; elementId: string | null } | null>(null);
|
|||
|
|
const [editingEntranceTech, setEditingEntranceTech] = useState<number | null>(null); // Редактирование тех. характеристик подъезда
|
|||
|
|
|
|||
|
|
// Группируем секции по типу
|
|||
|
|
const entranceSections = sections.filter(s => s.type === 'entrance');
|
|||
|
|
const floorSections = sections.filter(s => s.type === 'floor');
|
|||
|
|
const liftSections = sections.filter(s => s.type === 'lift');
|
|||
|
|
|
|||
|
|
const handleAddElement = (sectionId: string, isCommon: boolean = false) => {
|
|||
|
|
const newElement: InspectionMKDElement = {
|
|||
|
|
id: `element-${Date.now()}`,
|
|||
|
|
name: '',
|
|||
|
|
generalStatus: 'NOT_SELECTED',
|
|||
|
|
electroStatus: 'NOT_SELECTED',
|
|||
|
|
weldingStatus: 'NOT_SELECTED',
|
|||
|
|
repairType: '',
|
|||
|
|
mainPhoto: undefined,
|
|||
|
|
additionalPhotos: []
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (isCommon) {
|
|||
|
|
const updated = commonSections.map(s =>
|
|||
|
|
s.id === sectionId
|
|||
|
|
? { ...s, elements: [...s.elements, newElement] }
|
|||
|
|
: s
|
|||
|
|
);
|
|||
|
|
onUpdateCommonSections(updated);
|
|||
|
|
} else {
|
|||
|
|
const updated = sections.map(s =>
|
|||
|
|
s.id === sectionId
|
|||
|
|
? { ...s, elements: [...s.elements, newElement] }
|
|||
|
|
: s
|
|||
|
|
);
|
|||
|
|
onUpdateSections(updated);
|
|||
|
|
|
|||
|
|
// Сохраняем элементы этажа в building, если это секция этажа
|
|||
|
|
const section = sections.find(s => s.id === sectionId);
|
|||
|
|
if (section && section.type === 'floor') {
|
|||
|
|
const match = sectionId.match(/entrance-(\d+)-floor-(\d+)/);
|
|||
|
|
if (match && onSaveToBuilding) {
|
|||
|
|
const entranceNum = parseInt(match[1]);
|
|||
|
|
const floorNum = parseInt(match[2]);
|
|||
|
|
const updatedSection = updated.find(s => s.id === sectionId);
|
|||
|
|
if (updatedSection) {
|
|||
|
|
onSaveToBuilding(null, {
|
|||
|
|
entranceNumber: entranceNum,
|
|||
|
|
floorNumber: floorNum,
|
|||
|
|
elements: updatedSection.elements
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
setEditingElement({ sectionId, elementId: newElement.id });
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleAddFromTemplate = (sectionId: string, isCommon: boolean = false) => {
|
|||
|
|
const existingNames = new Set(
|
|||
|
|
isCommon
|
|||
|
|
? (commonSections.find(s => s.id === sectionId)?.elements || []).map(e => e.name)
|
|||
|
|
: (sections.find(s => s.id === sectionId)?.elements || []).map(e => e.name)
|
|||
|
|
);
|
|||
|
|
let namesToAdd: string[] = [];
|
|||
|
|
let prefix = sectionId;
|
|||
|
|
if (isCommon) {
|
|||
|
|
const section = commonSections.find(s => s.id === sectionId);
|
|||
|
|
if (section) {
|
|||
|
|
const templateNames = COMMON_SECTION_ELEMENTS[section.title];
|
|||
|
|
if (templateNames) namesToAdd = templateNames.filter(n => !existingNames.has(n));
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
const section = sections.find(s => s.id === sectionId);
|
|||
|
|
if (section) {
|
|||
|
|
if (section.type === 'floor') {
|
|||
|
|
const match = sectionId.match(/entrance-(\d+)-floor-(\d+)/);
|
|||
|
|
if (match) {
|
|||
|
|
namesToAdd = FLOOR_ELEMENT_NAMES.filter(n => !existingNames.has(n));
|
|||
|
|
prefix = `entrance-${match[1]}-floor-${match[2]}`;
|
|||
|
|
}
|
|||
|
|
} else if (section.type === 'entrance') {
|
|||
|
|
namesToAdd = ENTRANCE_ELEMENT_NAMES.filter(n => !existingNames.has(n));
|
|||
|
|
prefix = sectionId;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (namesToAdd.length === 0) return;
|
|||
|
|
const newElements = buildElementsFromNames(namesToAdd, prefix);
|
|||
|
|
if (isCommon) {
|
|||
|
|
const updated = commonSections.map(s =>
|
|||
|
|
s.id === sectionId ? { ...s, elements: [...s.elements, ...newElements] } : s
|
|||
|
|
);
|
|||
|
|
onUpdateCommonSections(updated);
|
|||
|
|
} else {
|
|||
|
|
const updated = sections.map(s =>
|
|||
|
|
s.id === sectionId ? { ...s, elements: [...s.elements, ...newElements] } : s
|
|||
|
|
);
|
|||
|
|
onUpdateSections(updated);
|
|||
|
|
const section = sections.find(s => s.id === sectionId);
|
|||
|
|
if (section?.type === 'floor') {
|
|||
|
|
const match = sectionId.match(/entrance-(\d+)-floor-(\d+)/);
|
|||
|
|
if (match && onSaveToBuilding) {
|
|||
|
|
const entranceNum = parseInt(match[1]);
|
|||
|
|
const floorNum = parseInt(match[2]);
|
|||
|
|
const updatedSection = updated.find(s => s.id === sectionId);
|
|||
|
|
if (updatedSection) {
|
|||
|
|
onSaveToBuilding(null, {
|
|||
|
|
entranceNumber: entranceNum,
|
|||
|
|
floorNumber: floorNum,
|
|||
|
|
elements: updatedSection.elements
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleUpdateElement = (sectionId: string, elementId: string, updates: Partial<InspectionMKDElement>, isCommon: boolean = false) => {
|
|||
|
|
if (isCommon) {
|
|||
|
|
const updated = commonSections.map(s =>
|
|||
|
|
s.id === sectionId
|
|||
|
|
? {
|
|||
|
|
...s,
|
|||
|
|
elements: s.elements.map(el =>
|
|||
|
|
el.id === elementId ? { ...el, ...updates } : el
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
: s
|
|||
|
|
);
|
|||
|
|
onUpdateCommonSections(updated);
|
|||
|
|
} else {
|
|||
|
|
const updated = sections.map(s =>
|
|||
|
|
s.id === sectionId
|
|||
|
|
? {
|
|||
|
|
...s,
|
|||
|
|
elements: s.elements.map(el =>
|
|||
|
|
el.id === elementId ? { ...el, ...updates } : el
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
: s
|
|||
|
|
);
|
|||
|
|
onUpdateSections(updated);
|
|||
|
|
|
|||
|
|
// Сохраняем элементы этажа в building, если это секция этажа
|
|||
|
|
const section = sections.find(s => s.id === sectionId);
|
|||
|
|
if (section && section.type === 'floor') {
|
|||
|
|
// Извлекаем номер подъезда и этажа из sectionId (формат: entrance-{num}-floor-{floor})
|
|||
|
|
const match = sectionId.match(/entrance-(\d+)-floor-(\d+)/);
|
|||
|
|
if (match && onSaveToBuilding) {
|
|||
|
|
const entranceNum = parseInt(match[1]);
|
|||
|
|
const floorNum = parseInt(match[2]);
|
|||
|
|
const updatedSection = updated.find(s => s.id === sectionId);
|
|||
|
|
if (updatedSection) {
|
|||
|
|
onSaveToBuilding(null, {
|
|||
|
|
entranceNumber: entranceNum,
|
|||
|
|
floorNumber: floorNum,
|
|||
|
|
elements: updatedSection.elements
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDeleteElement = (sectionId: string, elementId: string, isCommon: boolean = false) => {
|
|||
|
|
if (isCommon) {
|
|||
|
|
const updated = commonSections.map(s =>
|
|||
|
|
s.id === sectionId
|
|||
|
|
? { ...s, elements: s.elements.filter(el => el.id !== elementId) }
|
|||
|
|
: s
|
|||
|
|
);
|
|||
|
|
onUpdateCommonSections(updated);
|
|||
|
|
} else {
|
|||
|
|
const updated = sections.map(s =>
|
|||
|
|
s.id === sectionId
|
|||
|
|
? { ...s, elements: s.elements.filter(el => el.id !== elementId) }
|
|||
|
|
: s
|
|||
|
|
);
|
|||
|
|
onUpdateSections(updated);
|
|||
|
|
|
|||
|
|
// Сохраняем элементы этажа в building, если это секция этажа
|
|||
|
|
const section = sections.find(s => s.id === sectionId);
|
|||
|
|
if (section && section.type === 'floor') {
|
|||
|
|
const match = sectionId.match(/entrance-(\d+)-floor-(\d+)/);
|
|||
|
|
if (match && onSaveToBuilding) {
|
|||
|
|
const entranceNum = parseInt(match[1]);
|
|||
|
|
const floorNum = parseInt(match[2]);
|
|||
|
|
const updatedSection = updated.find(s => s.id === sectionId);
|
|||
|
|
if (updatedSection) {
|
|||
|
|
onSaveToBuilding(null, {
|
|||
|
|
entranceNumber: entranceNum,
|
|||
|
|
floorNumber: floorNum,
|
|||
|
|
elements: updatedSection.elements
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleToggleSection = (sectionId: string, isCommon: boolean = false) => {
|
|||
|
|
if (isCommon) {
|
|||
|
|
const updated = commonSections.map(s =>
|
|||
|
|
s.id === sectionId ? { ...s, isCollapsed: !s.isCollapsed } : s
|
|||
|
|
);
|
|||
|
|
onUpdateCommonSections(updated);
|
|||
|
|
} else {
|
|||
|
|
const updated = sections.map(s =>
|
|||
|
|
s.id === sectionId ? { ...s, isCollapsed: !s.isCollapsed } : s
|
|||
|
|
);
|
|||
|
|
onUpdateSections(updated);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const renderElement = (element: InspectionMKDElement, sectionId: string, isCommon: boolean = false) => {
|
|||
|
|
const isEditing = editingElement?.sectionId === sectionId && editingElement?.elementId === element.id;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div key={element.id} className="bg-slate-50 p-4 rounded-xl border border-slate-200 shadow-sm transition-all mb-3">
|
|||
|
|
<div className="flex justify-between items-start mb-3">
|
|||
|
|
{isEditing ? (
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={element.name}
|
|||
|
|
onChange={(e) => handleUpdateElement(sectionId, element.id, { name: e.target.value }, isCommon)}
|
|||
|
|
placeholder="Название элемента МКД"
|
|||
|
|
className="flex-1 px-3 py-2 bg-white border border-slate-200 rounded-xl text-sm font-bold focus:ring-2 focus:ring-primary-500 outline-none"
|
|||
|
|
autoFocus
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<div className="flex items-center gap-2 flex-1">
|
|||
|
|
<h4 className="font-bold text-sm text-slate-800 flex items-center gap-2">
|
|||
|
|
<Layers className="w-4 h-4 text-slate-400"/>
|
|||
|
|
{element.name || 'Новый элемент'}
|
|||
|
|
</h4>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setEditingElement({ sectionId, elementId: element.id })}
|
|||
|
|
className="text-xs text-primary-600 hover:text-primary-700 font-bold px-2 py-1 hover:bg-primary-50 rounded-lg transition-colors"
|
|||
|
|
>
|
|||
|
|
+ Изменить
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleDeleteElement(sectionId, element.id, isCommon)}
|
|||
|
|
className="text-red-500 hover:text-red-700 p-1 hover:bg-red-50 rounded-lg transition-colors"
|
|||
|
|
>
|
|||
|
|
<Trash2 className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="grid grid-cols-3 gap-3 mb-3">
|
|||
|
|
<div>
|
|||
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-1">Общий строй</label>
|
|||
|
|
<select
|
|||
|
|
value={element.generalStatus}
|
|||
|
|
onChange={(e) => handleUpdateElement(sectionId, element.id, { generalStatus: e.target.value as any }, isCommon)}
|
|||
|
|
className="w-full px-3 py-2 bg-white border border-slate-200 rounded-xl text-xs focus:ring-2 focus:ring-primary-500 outline-none"
|
|||
|
|
>
|
|||
|
|
<option value="NOT_SELECTED">Выбрать</option>
|
|||
|
|
<option value="OK">Удовлет</option>
|
|||
|
|
<option value="WARNING">Неуд</option>
|
|||
|
|
<option value="CRITICAL">Критично</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-1">Электроснабжение</label>
|
|||
|
|
<select
|
|||
|
|
value={element.electroStatus}
|
|||
|
|
onChange={(e) => handleUpdateElement(sectionId, element.id, { electroStatus: e.target.value as any }, isCommon)}
|
|||
|
|
className="w-full px-3 py-2 bg-white border border-slate-200 rounded-xl text-xs focus:ring-2 focus:ring-primary-500 outline-none"
|
|||
|
|
>
|
|||
|
|
<option value="NOT_SELECTED">Выбрать</option>
|
|||
|
|
<option value="OK">Удовлет</option>
|
|||
|
|
<option value="WARNING">Неуд</option>
|
|||
|
|
<option value="CRITICAL">Критично</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-1">Слесарные, сварочные</label>
|
|||
|
|
<select
|
|||
|
|
value={element.weldingStatus}
|
|||
|
|
onChange={(e) => handleUpdateElement(sectionId, element.id, { weldingStatus: e.target.value as any }, isCommon)}
|
|||
|
|
className="w-full px-3 py-2 bg-white border border-slate-200 rounded-xl text-xs focus:ring-2 focus:ring-primary-500 outline-none"
|
|||
|
|
>
|
|||
|
|
<option value="NOT_SELECTED">Выбрать</option>
|
|||
|
|
<option value="OK">Удовлет</option>
|
|||
|
|
<option value="WARNING">Неуд</option>
|
|||
|
|
<option value="CRITICAL">Критично</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="mb-3">
|
|||
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-1">Вид требуемого ремонта</label>
|
|||
|
|
<textarea
|
|||
|
|
value={element.repairType || ''}
|
|||
|
|
onChange={(e) => handleUpdateElement(sectionId, element.id, { repairType: e.target.value }, isCommon)}
|
|||
|
|
placeholder="Опишите требуемый ремонт..."
|
|||
|
|
className="w-full px-3 py-2 bg-white border border-slate-200 rounded-xl text-xs min-h-[60px] focus:ring-2 focus:ring-primary-500 outline-none"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="mt-2 space-y-2">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-tight">Фотофиксация состояния:</span>
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
<button className="flex items-center gap-1.5 px-3 py-1 bg-white border border-slate-200 rounded-lg text-[10px] font-bold text-slate-600 hover:text-primary-600 hover:border-primary-200 transition-colors shadow-sm">
|
|||
|
|
<Camera className="w-3.5 h-3.5 text-primary-500"/> Гл. фото
|
|||
|
|
</button>
|
|||
|
|
<button className="flex items-center gap-1.5 px-3 py-1 bg-white border border-slate-200 rounded-lg text-[10px] font-bold text-slate-600 hover:text-primary-600 hover:border-primary-200 transition-colors shadow-sm">
|
|||
|
|
<Camera className="w-3.5 h-3.5 text-primary-500"/> Доп. фото
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{(element.mainPhoto || (element.additionalPhotos && element.additionalPhotos.length > 0)) && (
|
|||
|
|
<div className="flex gap-2 overflow-x-auto no-scrollbar py-1">
|
|||
|
|
{element.mainPhoto && (
|
|||
|
|
<div className="w-16 h-16 rounded-lg relative flex-shrink-0 group overflow-hidden border border-slate-200 bg-slate-200">
|
|||
|
|
<img src={element.mainPhoto} alt="Main photo" className="w-full h-full object-cover" />
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleUpdateElement(sectionId, element.id, { mainPhoto: undefined }, isCommon)}
|
|||
|
|
className="absolute top-0.5 right-0.5 p-1 bg-red-500/80 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
|
|||
|
|
>
|
|||
|
|
<X className="w-2.5 h-2.5"/>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{element.additionalPhotos?.map((photo, pIdx) => (
|
|||
|
|
<div key={pIdx} className="w-16 h-16 rounded-lg relative flex-shrink-0 group overflow-hidden border border-slate-200 bg-slate-200">
|
|||
|
|
<img src={photo} alt="Additional photo" className="w-full h-full object-cover" />
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
const updated = element.additionalPhotos?.filter((_, i) => i !== pIdx) || [];
|
|||
|
|
handleUpdateElement(sectionId, element.id, { additionalPhotos: updated }, isCommon);
|
|||
|
|
}}
|
|||
|
|
className="absolute top-0.5 right-0.5 p-1 bg-red-500/80 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
|
|||
|
|
>
|
|||
|
|
<X className="w-2.5 h-2.5"/>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
{/* Табы для подъездов */}
|
|||
|
|
{entranceSections.length > 0 && (
|
|||
|
|
<div className="mb-6">
|
|||
|
|
<h3 className="font-bold text-slate-800 text-sm mb-3 flex items-center gap-2">
|
|||
|
|
<DoorClosed className="w-4 h-4 text-primary-500" /> Парадная
|
|||
|
|
</h3>
|
|||
|
|
<div className="flex gap-2 overflow-x-auto pb-2">
|
|||
|
|
{entranceSections.map(section => (
|
|||
|
|
<button
|
|||
|
|
key={section.id}
|
|||
|
|
onClick={() => setActiveTab(section.id)}
|
|||
|
|
className={`px-4 py-2 rounded-lg font-bold text-sm whitespace-nowrap transition-all ${
|
|||
|
|
activeTab === section.id
|
|||
|
|
? 'bg-primary-600 text-white shadow-lg'
|
|||
|
|
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
{section.number}
|
|||
|
|
</button>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
{activeTab && entranceSections.find(s => s.id === activeTab) && (() => {
|
|||
|
|
const currentSection = entranceSections.find(s => s.id === activeTab)!;
|
|||
|
|
const entrance = building.entrances?.find(e => e.number === currentSection.number);
|
|||
|
|
const techChars = entrance?.technicalCharacteristics || {};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="mt-4 space-y-4">
|
|||
|
|
{/* Технические характеристики подъезда */}
|
|||
|
|
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm mb-4">
|
|||
|
|
<div className="flex justify-between items-center mb-4">
|
|||
|
|
<h4 className="font-bold text-slate-800 text-sm">
|
|||
|
|
ТЕХНИЧЕСКИЕ ХАРАКТЕРИСТИКИ ПОДЪЕЗДА {currentSection.number}
|
|||
|
|
</h4>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setEditingEntranceTech(editingEntranceTech === currentSection.number ? null : currentSection.number)}
|
|||
|
|
className="text-primary-600 hover:text-primary-700 text-xs font-bold"
|
|||
|
|
>
|
|||
|
|
{editingEntranceTech === currentSection.number ? 'Сохранить' : 'Редактировать'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|||
|
|
<div>
|
|||
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-1">Наличие подвала</label>
|
|||
|
|
{editingEntranceTech === currentSection.number ? (
|
|||
|
|
<select
|
|||
|
|
value={techChars.hasBasement || 'NOT_SELECTED'}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
if (onSaveToBuilding) {
|
|||
|
|
onSaveToBuilding({
|
|||
|
|
entranceNumber: currentSection.number,
|
|||
|
|
technicalCharacteristics: {
|
|||
|
|
...techChars,
|
|||
|
|
hasBasement: e.target.value as any
|
|||
|
|
}
|
|||
|
|
}, null);
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="w-full px-2 py-1.5 border border-slate-300 rounded-lg text-xs"
|
|||
|
|
>
|
|||
|
|
<option value="NOT_SELECTED">Выбрать</option>
|
|||
|
|
<option value="YES">Да</option>
|
|||
|
|
<option value="NO">Нет</option>
|
|||
|
|
</select>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-sm font-bold text-slate-800">{techChars.hasBasement === 'YES' ? 'Да' : techChars.hasBasement === 'NO' ? 'Нет' : '-'}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-1">Наличие тех. этажа</label>
|
|||
|
|
{editingEntranceTech === currentSection.number ? (
|
|||
|
|
<select
|
|||
|
|
value={techChars.hasTechFloor || 'NOT_SELECTED'}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
if (onSaveToBuilding) {
|
|||
|
|
onSaveToBuilding({
|
|||
|
|
entranceNumber: currentSection.number,
|
|||
|
|
technicalCharacteristics: {
|
|||
|
|
...techChars,
|
|||
|
|
hasTechFloor: e.target.value as any
|
|||
|
|
}
|
|||
|
|
}, null);
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="w-full px-2 py-1.5 border border-slate-300 rounded-lg text-xs"
|
|||
|
|
>
|
|||
|
|
<option value="NOT_SELECTED">Выбрать</option>
|
|||
|
|
<option value="YES">Да</option>
|
|||
|
|
<option value="NO">Нет</option>
|
|||
|
|
</select>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-sm font-bold text-slate-800">{techChars.hasTechFloor === 'YES' ? 'Да' : techChars.hasTechFloor === 'NO' ? 'Нет' : '-'}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-1">Наличие паркинга</label>
|
|||
|
|
{editingEntranceTech === currentSection.number ? (
|
|||
|
|
<select
|
|||
|
|
value={techChars.hasParking || 'NOT_SELECTED'}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
if (onSaveToBuilding) {
|
|||
|
|
onSaveToBuilding({
|
|||
|
|
entranceNumber: currentSection.number,
|
|||
|
|
technicalCharacteristics: {
|
|||
|
|
...techChars,
|
|||
|
|
hasParking: e.target.value as any
|
|||
|
|
}
|
|||
|
|
}, null);
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="w-full px-2 py-1.5 border border-slate-300 rounded-lg text-xs"
|
|||
|
|
>
|
|||
|
|
<option value="NOT_SELECTED">Выбрать</option>
|
|||
|
|
<option value="YES">Да</option>
|
|||
|
|
<option value="NO">Нет</option>
|
|||
|
|
</select>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-sm font-bold text-slate-800">{techChars.hasParking === 'YES' ? 'Да' : techChars.hasParking === 'NO' ? 'Нет' : '-'}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-1">Наличие чердака</label>
|
|||
|
|
{editingEntranceTech === currentSection.number ? (
|
|||
|
|
<select
|
|||
|
|
value={techChars.hasAttic || 'NOT_SELECTED'}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
if (onSaveToBuilding) {
|
|||
|
|
onSaveToBuilding({
|
|||
|
|
entranceNumber: currentSection.number,
|
|||
|
|
technicalCharacteristics: {
|
|||
|
|
...techChars,
|
|||
|
|
hasAttic: e.target.value as any
|
|||
|
|
}
|
|||
|
|
}, null);
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="w-full px-2 py-1.5 border border-slate-300 rounded-lg text-xs"
|
|||
|
|
>
|
|||
|
|
<option value="NOT_SELECTED">Выбрать</option>
|
|||
|
|
<option value="YES">Да</option>
|
|||
|
|
<option value="NO">Нет</option>
|
|||
|
|
</select>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-sm font-bold text-slate-800">{techChars.hasAttic === 'YES' ? 'Да' : techChars.hasAttic === 'NO' ? 'Нет' : '-'}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-1">Количество этажей</label>
|
|||
|
|
{editingEntranceTech === currentSection.number ? (
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={techChars.floorsCount || entrance?.floors || ''}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
if (onSaveToBuilding) {
|
|||
|
|
onSaveToBuilding({
|
|||
|
|
entranceNumber: currentSection.number,
|
|||
|
|
technicalCharacteristics: {
|
|||
|
|
...techChars,
|
|||
|
|
floorsCount: parseInt(e.target.value) || undefined
|
|||
|
|
}
|
|||
|
|
}, null);
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="w-full px-2 py-1.5 border border-slate-300 rounded-lg text-xs"
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-sm font-bold text-slate-800">{techChars.floorsCount || entrance?.floors || '-'}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-1">Количество лифтов</label>
|
|||
|
|
{editingEntranceTech === currentSection.number ? (
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={techChars.liftsCount || entrance?.liftsCount || ''}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
if (onSaveToBuilding) {
|
|||
|
|
onSaveToBuilding({
|
|||
|
|
entranceNumber: currentSection.number,
|
|||
|
|
technicalCharacteristics: {
|
|||
|
|
...techChars,
|
|||
|
|
liftsCount: parseInt(e.target.value) || undefined
|
|||
|
|
}
|
|||
|
|
}, null);
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="w-full px-2 py-1.5 border border-slate-300 rounded-lg text-xs"
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-sm font-bold text-slate-800">{techChars.liftsCount || entrance?.liftsCount || '-'}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-1">Наличие мансарды</label>
|
|||
|
|
{editingEntranceTech === currentSection.number ? (
|
|||
|
|
<select
|
|||
|
|
value={techChars.hasMansard || 'NOT_SELECTED'}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
if (onSaveToBuilding) {
|
|||
|
|
onSaveToBuilding({
|
|||
|
|
entranceNumber: currentSection.number,
|
|||
|
|
technicalCharacteristics: {
|
|||
|
|
...techChars,
|
|||
|
|
hasMansard: e.target.value as any
|
|||
|
|
}
|
|||
|
|
}, null);
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="w-full px-2 py-1.5 border border-slate-300 rounded-lg text-xs"
|
|||
|
|
>
|
|||
|
|
<option value="NOT_SELECTED">Выбрать</option>
|
|||
|
|
<option value="YES">Да</option>
|
|||
|
|
<option value="NO">Нет</option>
|
|||
|
|
</select>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-sm font-bold text-slate-800">{techChars.hasMansard === 'YES' ? 'Да' : techChars.hasMansard === 'NO' ? 'Нет' : '-'}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-1">Кол-во входных групп</label>
|
|||
|
|
{editingEntranceTech === currentSection.number ? (
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={techChars.entranceGroupsCount || entrance?.entranceGroupsCount || ''}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
if (onSaveToBuilding) {
|
|||
|
|
onSaveToBuilding({
|
|||
|
|
entranceNumber: currentSection.number,
|
|||
|
|
technicalCharacteristics: {
|
|||
|
|
...techChars,
|
|||
|
|
entranceGroupsCount: parseInt(e.target.value) || undefined
|
|||
|
|
}
|
|||
|
|
}, null);
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="w-full px-2 py-1.5 border border-slate-300 rounded-lg text-xs"
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-sm font-bold text-slate-800">{techChars.entranceGroupsCount || entrance?.entranceGroupsCount || '-'}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Элементы МКД подъезда */}
|
|||
|
|
{currentSection.elements.map(el => renderElement(el, currentSection.id, false))}
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleAddElement(currentSection.id, false)}
|
|||
|
|
className="flex-1 py-3 bg-primary-600 text-white rounded-xl font-bold text-sm flex items-center justify-center gap-2 shadow-lg shadow-primary-500/20 hover:bg-primary-700 transition-all"
|
|||
|
|
>
|
|||
|
|
<Plus className="w-4 h-4" /> ДОБАВИТЬ ЭЛЕМЕНТ МКД
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => handleAddFromTemplate(currentSection.id, false)}
|
|||
|
|
className="py-3 px-4 bg-white border border-slate-200 text-slate-700 rounded-xl font-bold text-sm flex items-center justify-center gap-2 hover:bg-slate-50 transition-all"
|
|||
|
|
title="Добавить недостающие пункты из типового чек-листа парадной"
|
|||
|
|
>
|
|||
|
|
<ListPlus className="w-4 h-4" /> Из шаблона
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})()}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Табы для этажей */}
|
|||
|
|
{floorSections.length > 0 && (
|
|||
|
|
<div className="mb-6">
|
|||
|
|
<h3 className="font-bold text-slate-800 text-sm mb-3 flex items-center gap-2">
|
|||
|
|
<Building2 className="w-4 h-4 text-primary-500" /> Этажи
|
|||
|
|
</h3>
|
|||
|
|
<div className="flex gap-2 overflow-x-auto pb-2 flex-wrap">
|
|||
|
|
{floorSections.map(section => {
|
|||
|
|
const match = section.id.match(/entrance-(\d+)-floor-(\d+)/);
|
|||
|
|
const entranceNum = match ? match[1] : '';
|
|||
|
|
const floorNum = section.number ?? '';
|
|||
|
|
const label = entranceNum ? `П${entranceNum} Этаж ${floorNum}` : `Этаж ${floorNum}`;
|
|||
|
|
return (
|
|||
|
|
<button
|
|||
|
|
key={section.id}
|
|||
|
|
onClick={() => setActiveTab(section.id)}
|
|||
|
|
className={`px-4 py-2 rounded-lg font-bold text-sm whitespace-nowrap transition-all ${
|
|||
|
|
activeTab === section.id
|
|||
|
|
? 'bg-primary-600 text-white shadow-lg'
|
|||
|
|
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
{label}
|
|||
|
|
</button>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
{activeTab && floorSections.find(s => s.id === activeTab) && (() => {
|
|||
|
|
const currentSection = floorSections.find(s => s.id === activeTab)!;
|
|||
|
|
// Извлекаем номер подъезда из sectionId
|
|||
|
|
const match = currentSection.id.match(/entrance-(\d+)-floor-(\d+)/);
|
|||
|
|
const entranceNum = match ? parseInt(match[1]) : null;
|
|||
|
|
const floorNum = currentSection.number;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="mt-4 space-y-4">
|
|||
|
|
<h4 className="font-bold text-slate-800 text-sm mb-2">
|
|||
|
|
ЭЛЕМЕНТЫ МКД ЭТАЖА {floorNum}{entranceNum ? ` (Подъезд ${entranceNum})` : ''}
|
|||
|
|
</h4>
|
|||
|
|
{currentSection.elements.map(el => renderElement(el, currentSection.id, false))}
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleAddElement(currentSection.id, false)}
|
|||
|
|
className="flex-1 py-3 bg-primary-600 text-white rounded-xl font-bold text-sm flex items-center justify-center gap-2 shadow-lg shadow-primary-500/20 hover:bg-primary-700 transition-all"
|
|||
|
|
>
|
|||
|
|
<Plus className="w-4 h-4" /> ДОБАВИТЬ ЭЛЕМЕНТ МКД
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => handleAddFromTemplate(currentSection.id, false)}
|
|||
|
|
className="py-3 px-4 bg-white border border-slate-200 text-slate-700 rounded-xl font-bold text-sm flex items-center justify-center gap-2 hover:bg-slate-50 transition-all"
|
|||
|
|
title="Добавить недостающие пункты из типового чек-листа этажа"
|
|||
|
|
>
|
|||
|
|
<ListPlus className="w-4 h-4" /> Из шаблона
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})()}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Секции для лифтов */}
|
|||
|
|
{liftSections.length > 0 && (
|
|||
|
|
<div className="mb-6">
|
|||
|
|
<h3 className="font-bold text-slate-800 text-sm mb-3 flex items-center gap-2">
|
|||
|
|
<Gauge className="w-4 h-4 text-primary-500" /> Лифт
|
|||
|
|
</h3>
|
|||
|
|
{liftSections.map(section => (
|
|||
|
|
<div key={section.id} className="mb-4 bg-slate-50 p-4 rounded-xl border border-slate-200 shadow-sm">
|
|||
|
|
<div className="flex justify-between items-center mb-4">
|
|||
|
|
<h4 className="font-bold text-slate-800 text-lg">Лифт {section.number}</h4>
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleToggleSection(section.id, false)}
|
|||
|
|
className="text-slate-500 hover:text-slate-700 transition-colors"
|
|||
|
|
>
|
|||
|
|
{section.isCollapsed ? <ChevronDown className="w-5 h-5" /> : <ChevronUp className="w-5 h-5" />}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
{!section.isCollapsed && (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
{section.elements.map(el => renderElement(el, section.id, false))}
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleAddElement(section.id, false)}
|
|||
|
|
className="w-full py-3 bg-primary-600 text-white rounded-xl font-bold text-sm flex items-center justify-center gap-2 shadow-lg shadow-primary-500/20 hover:bg-primary-700 transition-all"
|
|||
|
|
>
|
|||
|
|
<Plus className="w-4 h-4" /> ДОБАВИТЬ ЭЛЕМЕНТ МКД
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Общие секции */}
|
|||
|
|
{commonSections.map(section => (
|
|||
|
|
<div key={section.id} className="bg-slate-50 p-4 rounded-xl border border-slate-200 shadow-sm mb-4">
|
|||
|
|
<div className="flex justify-between items-center mb-4">
|
|||
|
|
<h3 className="font-bold text-slate-800 text-sm">{section.title}</h3>
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleToggleSection(section.id, true)}
|
|||
|
|
className="text-slate-500 hover:text-slate-700 transition-colors"
|
|||
|
|
>
|
|||
|
|
{section.isCollapsed ? <ChevronDown className="w-5 h-5" /> : <ChevronUp className="w-5 h-5" />}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
{!section.isCollapsed && (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
{section.elements.map(el => renderElement(el, section.id, true))}
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleAddElement(section.id, true)}
|
|||
|
|
className="flex-1 py-3 bg-primary-600 text-white rounded-xl font-bold text-sm flex items-center justify-center gap-2 shadow-lg shadow-primary-500/20 hover:bg-primary-700 transition-all"
|
|||
|
|
>
|
|||
|
|
<Plus className="w-4 h-4" /> ДОБАВИТЬ ЭЛЕМЕНТ МКД
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => handleAddFromTemplate(section.id, true)}
|
|||
|
|
className="py-3 px-4 bg-white border border-slate-200 text-slate-700 rounded-xl font-bold text-sm flex items-center justify-center gap-2 hover:bg-slate-50 transition-all"
|
|||
|
|
title="Добавить недостающие пункты из типового чек-листа"
|
|||
|
|
>
|
|||
|
|
<ListPlus className="w-4 h-4" /> Из шаблона
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|