commit de94ad707b4a51b7f3f9cd642ec703579c712728 Author: Arsen Date: Wed Feb 4 00:17:04 2026 +0500 Initial commit MKD fixes diff --git a/.env.example b/.env.example new file mode 100755 index 0000000..dfbc36a --- /dev/null +++ b/.env.example @@ -0,0 +1,27 @@ +# API Configuration — ваш Node/Express бэкенд (НЕ n8n). +# Локально: http://localhost:4000/api или /api (если включён proxy в vite.config). +# В dev при /api запросы идут через Vite proxy на localhost:4000. +VITE_API_BASE_URL=http://localhost:4000/api + +# Auth (backend) +JWT_SECRET=your-secret-change-in-production +JWT_EXPIRES_IN=7d + +# Turnstile captcha (optional): https://dash.cloudflare.com/?to=/:account/turnstile +TURNSTILE_SECRET_KEY= +VITE_TURNSTILE_SITE_KEY= + +# Doma AI API Configuration (PRODUCTION) +# ВАЖНО: Укажите URL вашего продакшн инстанса Doma AI +# Пример для продакшена: https://your-domain.doma.ai/admin/api +# Для тестирования можно использовать: https://condo.d.doma.ai/admin/api +VITE_DOMA_AI_API_URL=https://your-domain.doma.ai/admin/api + +# Учетные данные для авторизации в Doma AI +# Используйте email и пароль ИЛИ телефон и пароль +# Для продакшена используйте учетные данные вашей организации в Doma AI +VITE_DOMA_AI_EMAIL=your-email@example.com +VITE_DOMA_AI_PASSWORD=your-password +# ИЛИ используйте телефон: +# VITE_DOMA_AI_PHONE=+79991234567 +# VITE_DOMA_AI_PASSWORD=your-password diff --git a/.env.production b/.env.production new file mode 100755 index 0000000..8c30a39 --- /dev/null +++ b/.env.production @@ -0,0 +1,13 @@ +VITE_API_BASE_URL=http://localhost:4000/api + +# Doma AI API Configuration (PRODUCTION) +# ВАЖНО: Укажите URL вашего продакшн инстанса Doma AI +# Пример: https://your-domain.doma.ai/admin/api +# Для тестирования можно использовать: https://condo.d.doma.ai/admin/api +VITE_DOMA_AI_API_URL= + +# Учетные данные для авторизации в Doma AI +# Используйте email и пароль ИЛИ телефон и пароль +VITE_DOMA_AI_EMAIL= +VITE_DOMA_AI_PASSWORD= +VITE_DOMA_AI_PHONE= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100755 index 0000000..52c0b6e --- /dev/null +++ b/App.tsx @@ -0,0 +1,1218 @@ + +import React, { useState, useEffect, lazy, Suspense } from 'react'; +import { Navigation } from './components/Navigation'; +import { Bell, ChevronDown, Settings, User as UserIcon, LogOut, X, Plug, UserCog, Building2 } from 'lucide-react'; +import { Building, User, UserRole } from './types'; +import { CURRENT_USER_MOCK, NAV_ITEMS } from './constants'; +import { storageService } from './services/storageService'; +import { backendApi, syncCachedRequests, getAuthToken, setAuthToken, clearAuth, fetchGuestToken } from './services/apiClient'; +import { ConnectionIndicator } from './components/ConnectionIndicator'; +import { NotificationPanel } from './components/NotificationPanel'; +import { connectionService } from './services/connectionService'; +import { settingsService, DomaAISettings } from './services/settingsService'; +import { apiClient } from './services/apiClient'; +import { PermissionsProvider } from './contexts/PermissionsContext'; +import { allowedSubsForSection } from './constants/permissions'; +import { ROLE_NAMES, ROLE_ACCESS } from './constants/roleAccess'; + +// Lazy-loaded modules (reduce initial bundle size) +const BuildingCharacteristics = lazy(() => import('./components/BuildingCharacteristics').then(m => ({ default: m.BuildingCharacteristics }))); +const DashboardNavigation = lazy(() => import('./components/DashboardNavigation').then(m => ({ default: m.DashboardNavigation }))); +const SummaryDashboard = lazy(() => import('./components/SummaryDashboard').then(m => ({ default: m.SummaryDashboard }))); +const HRModule = lazy(() => import('./components/HRModule').then(m => ({ default: m.HRModule }))); +const ApplicationsModule = lazy(() => import('./components/ApplicationsModule').then(m => ({ default: m.ApplicationsModule }))); +const PRModule = lazy(() => import('./components/PRModule').then(m => ({ default: m.PRModule }))); +const FinanceModule = lazy(() => import('./components/FinanceModule').then(m => ({ default: m.FinanceModule }))); +const OfficeModule = lazy(() => import('./components/OfficeModule').then(m => ({ default: m.OfficeModule }))); +const LegalModule = lazy(() => import('./components/LegalModule').then(m => ({ default: m.LegalModule }))); +const DevelopmentModule = lazy(() => import('./components/DevelopmentModule').then(m => ({ default: m.DevelopmentModule }))); +const AdminModule = lazy(() => import('./components/AdminModule').then(m => ({ default: m.AdminModule }))); +const BuildingReportPage = lazy(() => import('./components/pr/BuildingReportPage').then(m => ({ default: m.BuildingReportPage }))); +const NPSSurveyPage = lazy(() => import('./components/pr/NPSSurveyPage').then(m => ({ default: m.NPSSurveyPage }))); +const LoginPage = lazy(() => import('./components/LoginPage').then(m => ({ default: m.LoginPage }))); +const ProfileSettingsModal = lazy(() => import('./components/ProfileSettingsModal').then(m => ({ default: m.ProfileSettingsModal }))); + +const LazyFallback = () => ( +
+
Загрузка...
+
+); + +const CURRENT_USER_STORAGE_KEY = 'mkd_currentUser'; +const INTEGRATION_ENABLED_KEY = 'mkd_integrationEnabled'; +const PORTAL_LOGIN_KEY = 'mkd_portalLogin'; + +export default function App() { + // ====== Публичные страницы отчетов (/reports/:id) ====== + const [pathname, setPathname] = useState(() => window.location.pathname); + const [isAuthenticated, setIsAuthenticated] = useState(null); + const [allowedSections, setAllowedSections] = useState([]); + /** Детальные права (разделы и подразделы/отчёты). null = по роли, все подразделы разрешены */ + const [userPermissions, setUserPermissions] = useState(null); + const [authError, setAuthError] = useState(null); + const [authLoading, setAuthLoading] = useState(false); + + const [activeTab, setActiveTab] = useState(() => { + const saved = localStorage.getItem('mkd_activeTab'); + return saved || 'dashboard'; + }); + const [currentUser, setCurrentUser] = useState(() => { + try { + const saved = localStorage.getItem(CURRENT_USER_STORAGE_KEY); + if (saved) { + return JSON.parse(saved) as User; + } + } catch (e) { + console.warn('[App] Не удалось прочитать сохранённый профиль пользователя, используем значения по умолчанию'); + } + return CURRENT_USER_MOCK; + }); + const [selectedBuilding, setSelectedBuilding] = useState(null); + const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [isProfileOpen, setIsProfileOpen] = useState(false); + const [isRoleAdminOpen, setIsRoleAdminOpen] = useState(false); + const [activeSettingsTab, setActiveSettingsTab] = useState<'role' | 'integrations'>('role'); + const [editableUser, setEditableUser] = useState(null); + const [integrationEnabled, setIntegrationEnabled] = useState(() => { + const saved = localStorage.getItem(INTEGRATION_ENABLED_KEY); + if (saved === 'false') return false; + return true; + }); + const [financeOpenInvoiceId, setFinanceOpenInvoiceId] = useState(null); + const [notificationUnreadCount, setNotificationUnreadCount] = useState(0); + const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false); + const [financeInvoicePrefill, setFinanceInvoicePrefill] = useState<{ + purposeType: 'event'; + purposeEventId?: string; + purposeDescription?: string; + totalAmount?: number; + } | null>(null); + const [pendingQuickAction, setPendingQuickAction] = useState(null); + const [dashboardOpenNewsId, setDashboardOpenNewsId] = useState(null); + + useEffect(() => { + const onPopState = () => setPathname(window.location.pathname); + window.addEventListener('popstate', onPopState); + return () => window.removeEventListener('popstate', onPopState); + }, []); + + // Восстановление сессии по токену; параллельно при необходимости загружаем список зданий для savedBuildingId + useEffect(() => { + if (pathname.startsWith('/reports/') || pathname.startsWith('/nps/')) { + return; + } + const token = getAuthToken(); + if (!token) { + setIsAuthenticated(false); + return; + } + const savedBuildingId = localStorage.getItem('mkd_selectedBuildingId'); + const needBuildings = !!savedBuildingId && !storageService.getBuildingById(savedBuildingId); + let cancelled = false; + const mePromise = backendApi.getMe(); + const buildingsPromise = needBuildings ? backendApi.getBuildings() : Promise.resolve(null); + Promise.all([mePromise, buildingsPromise]) + .then(([me, buildings]) => { + if (cancelled) return; + setCurrentUser({ + ...me, + id: me.id, + name: me.name, + role: me.role as UserRole, + avatar: me.avatar || 'https://picsum.photos/id/1005/64/64', + }); + setAllowedSections(me.allowedSections || []); + setUserPermissions(me.permissions ?? null); + setIsAuthenticated(true); + setAuthError(null); + if (needBuildings && buildings) { + const found = buildings.find((b: Building) => b.id === savedBuildingId); + if (found) setSelectedBuilding(found); + else localStorage.removeItem('mkd_selectedBuildingId'); + } + }) + .catch(() => { + if (!cancelled) { + setIsAuthenticated(false); + setAuthError(null); + if (needBuildings) localStorage.removeItem('mkd_selectedBuildingId'); + } + }); + return () => { cancelled = true; }; + }, [pathname]); + + // Установка selectedBuilding из localStorage, если здание уже есть в storage (без запроса к API) + useEffect(() => { + const savedBuildingId = localStorage.getItem('mkd_selectedBuildingId'); + if (savedBuildingId) { + const building = storageService.getBuildingById(savedBuildingId); + if (building) setSelectedBuilding(building); + } + }, []); + + useEffect(() => { + const onLogout = () => setIsAuthenticated(false); + window.addEventListener('mkd-auth-logout', onLogout); + return () => window.removeEventListener('mkd-auth-logout', onLogout); + }, []); + + // --- All hooks must run before any conditional return (Rules of Hooks) --- + type FinanceApproverRole = 'manager' | 'finance_manager' | 'financier' | 'finance_director' | 'director' | 'top_management'; + type FinanceUserRoleRow = { id: number; userId: string; role: FinanceApproverRole; createdAt?: string; updatedAt?: string }; + + const [roleAdminUserId, setRoleAdminUserId] = useState('user-1'); + const [roleAdminRoles, setRoleAdminRoles] = useState([]); + const [roleAdminLoading, setRoleAdminLoading] = useState(false); + const [roleAdminSelectedRole, setRoleAdminSelectedRole] = useState('manager'); + + const [domaAISettings, setDomaAISettings] = useState(() => { + const saved = settingsService.getDomaAISettings(); + return saved || { + apiUrl: '', + token: '', + }; + }); + + // --- All useEffects must run before any conditional return (Rules of Hooks) --- + useEffect(() => { + if (isSettingsOpen) { + backendApi.getDomaSettings() + .then((data) => { + setDomaAISettings({ apiUrl: data.apiUrl || '', token: data.token || '' }); + }) + .catch(() => { + const saved = settingsService.getDomaAISettings(); + if (saved) setDomaAISettings(saved); + }); + } + }, [isSettingsOpen]); + + useEffect(() => { + if (isProfileOpen) { + setEditableUser(currentUser); + } + }, [isProfileOpen, currentUser]); + + useEffect(() => { + try { + localStorage.setItem(CURRENT_USER_STORAGE_KEY, JSON.stringify(currentUser)); + } catch (e) { + console.warn('[App] Не удалось сохранить профиль пользователя в localStorage'); + } + }, [currentUser]); + + useEffect(() => { + localStorage.setItem('mkd_activeTab', activeTab); + }, [activeTab]); + + const fetchNotificationUnreadCount = React.useCallback(async () => { + if (!getAuthToken()) return; + try { + const { count } = await backendApi.getUnreadCount(); + setNotificationUnreadCount(count); + } catch { + setNotificationUnreadCount(0); + } + }, []); + + useEffect(() => { + fetchNotificationUnreadCount(); + const interval = setInterval(fetchNotificationUnreadCount, 60000); + const onFocus = () => fetchNotificationUnreadCount(); + window.addEventListener('focus', onFocus); + return () => { + clearInterval(interval); + window.removeEventListener('focus', onFocus); + }; + }, [fetchNotificationUnreadCount]); + + useEffect(() => { + const allowed = allowedSections.length > 0 + ? (allowedSections.includes('all') ? ['dashboard', 'objects', 'requests', 'pr', 'finance', 'legal', 'development', 'hr', 'office', 'admin'] : allowedSections) + : ROLE_ACCESS[currentUser.role]; + if (allowed && !allowed.includes('all') && !allowed.includes(activeTab)) { + const firstAllowed = NAV_ITEMS.find(item => allowed.includes(item.id))?.id || 'dashboard'; + if (firstAllowed && firstAllowed !== activeTab) { + setActiveTab(firstAllowed); + } + } + }, [currentUser.role, activeTab, allowedSections]); + + useEffect(() => { + localStorage.setItem(INTEGRATION_ENABLED_KEY, String(integrationEnabled)); + }, [integrationEnabled]); + + useEffect(() => { + let lastStatus = connectionService.getStatus(); + const unsubscribe = connectionService.subscribe((status) => { + if (lastStatus === 'disconnected' && status === 'connected') { + syncCachedRequests().then((result) => { + if (result.success > 0) { + console.log(`Синхронизировано ${result.success} запросов из кэша`); + } + }).catch((error) => { + console.error('Ошибка синхронизации кэша:', error); + }); + } + lastStatus = status; + }); + return () => { unsubscribe(); }; + }, []); + + useEffect(() => { + const handler = (event: any) => { + const d = event?.detail; + if (d?.invoiceId) { + setFinanceOpenInvoiceId(d.invoiceId); + setFinanceInvoicePrefill(null); + setActiveTab('finance'); + return; + } + if (d?.purposeType === 'event' || d?.purposeEventId) { + setFinanceOpenInvoiceId(null); + setFinanceInvoicePrefill({ + purposeType: 'event', + purposeEventId: d?.purposeEventId, + purposeDescription: d?.purposeDescription ?? '', + totalAmount: d?.totalAmount + }); + setActiveTab('finance'); + } + }; + window.addEventListener('mkd-open-finance-invoice', handler as EventListener); + return () => window.removeEventListener('mkd-open-finance-invoice', handler as EventListener); + }, []); + + // Если открыли публичную ссылку на отчет - рендерим сразу опубликованную версию без портала + if (pathname.startsWith('/reports/')) { + const parts = pathname.split('/').filter(Boolean); + const reportId = parts[1] || 'demo'; + + // Компонент для загрузки отчета и отображения + const PublishedReportLoader = () => { + const [reportInfo, setReportInfo] = React.useState<{ address: string; buildingId?: string; month?: string } | null>(null); + const [isLoading, setIsLoading] = React.useState(true); + + React.useEffect(() => { + const loadReport = async () => { + if (reportId === 'demo') { + setReportInfo({ address: 'Кавказская, 12' }); + setIsLoading(false); + return; + } + + try { + // Гость без учётки: если нет токена — получаем гостевой (доступ только к отчётам) + if (!getAuthToken()) { + const guestToken = await fetchGuestToken(); + setAuthToken(guestToken); + } + const report = await apiClient.get(`/pr/reports/${reportId}`); + console.log('[PublishedReportLoader] Загружен отчет:', { + reportId, + hasAddress: !!report.address, + hasContent: !!report.content, + hasBuildingData: !!report.building_data, + contentBuildingAddress: report.content?.building?.address + }); + + // Извлекаем адрес из разных возможных мест (приоритет: content.building.address > address > building_data) + let address = report.content?.building?.address || + report.address || + (report.building_data?.passport?.address) || + (report.building_data?.passport?.general?.address); + + // Если адрес все еще не найден, используем fallback + if (!address) { + address = `Отчет ${reportId}`; + console.warn('[PublishedReportLoader] Адрес не найден, используем fallback'); + } + + setReportInfo({ + address: address, + buildingId: report.buildingId || report.building_id, + month: report.month + }); + } catch (err) { + console.error('Error loading report:', err); + setReportInfo({ address: `Отчет ${reportId}` }); + } finally { + setIsLoading(false); + } + }; + + loadReport(); + }, [reportId]); + + if (isLoading) { + return ; + } + + return ( + + ); + }; + + return ( + }> + + + ); + } + + // Если открыли публичную ссылку на NPS опрос - рендерим страницу опроса без портала + if (pathname.startsWith('/nps/')) { + const parts = pathname.split('/').filter(Boolean); + const surveyId = parts[1] || null; + + if (surveyId) { + // Получаем номер квартиры из URL параметров + const urlParams = new URLSearchParams(window.location.search); + const apartment = urlParams.get('apartment'); + + return ( + }> + + + ); + } + } + + // Экран входа: нет токена или сессия не восстановлена + if (isAuthenticated === false) { + return ( + }> + { + setCurrentUser({ + id: user.id, + name: user.name, + role: user.role as UserRole, + avatar: user.avatar || 'https://picsum.photos/id/1005/64/64', + }); + backendApi.getMe().then((me) => { + setAllowedSections(me.allowedSections || []); + setUserPermissions(me.permissions ?? null); + }).catch(() => {}); + setIsAuthenticated(true); + setAuthError(null); + }} + /> + + ); + } + + // Загрузка сессии (проверка токена) + if (isAuthenticated === null) { + return ( +
+
Загрузка...
+
+ ); + } + + // ====== Админка ролей согласования счетов (скрыто в меню пользователя) ====== + const loadUserApproverRoles = async (userId: string) => { + try { + setRoleAdminLoading(true); + const roles = await apiClient.get(`/finance/user-roles?userId=${encodeURIComponent(userId)}`); + setRoleAdminRoles(Array.isArray(roles) ? roles : []); + } catch (err: any) { + console.error('Error loading user roles:', err); + setRoleAdminRoles([]); + alert(err?.message || 'Ошибка загрузки ролей пользователя'); + } finally { + setRoleAdminLoading(false); + } + }; + + const addUserApproverRole = async (userId: string, role: FinanceApproverRole) => { + try { + setRoleAdminLoading(true); + await apiClient.post(`/finance/user-roles`, { userId, role }); + await loadUserApproverRoles(userId); + } catch (err: any) { + console.error('Error adding user role:', err); + alert(err?.message || 'Ошибка добавления роли'); + } finally { + setRoleAdminLoading(false); + } + }; + + const removeUserApproverRole = async (id: number) => { + try { + setRoleAdminLoading(true); + await apiClient.delete(`/finance/user-roles/${id}`); + await loadUserApproverRoles(roleAdminUserId); + } catch (err: any) { + console.error('Error removing user role:', err); + alert(err?.message || 'Ошибка удаления роли'); + } finally { + setRoleAdminLoading(false); + } + }; + + const handleSelectBuilding = (building: Building) => { + setSelectedBuilding(building); + // Сохраняем ID выбранного здания в localStorage + localStorage.setItem('mkd_selectedBuildingId', building.id); + }; + + const handleBackToDashboard = () => { + setSelectedBuilding(null); + // Удаляем сохраненное здание из localStorage + localStorage.removeItem('mkd_selectedBuildingId'); + }; + + // Demo function to switch roles + const toggleRole = () => { + const roles: UserRole[] = ['DIRECTOR', 'ENGINEER', 'MASTER', 'LAWYER', 'FINANCIER', 'HR_MANAGER', 'PR_MANAGER']; + const nextIndex = (roles.indexOf(currentUser.role) + 1) % roles.length; + const newRole = roles[nextIndex]; + + // Update user role + setCurrentUser({ ...currentUser, role: newRole }); + }; + + const handleLogout = () => { + if (confirm('Вы уверены, что хотите выйти?')) { + clearAuth(); + setCurrentUser(CURRENT_USER_MOCK); + setAllowedSections([]); + setIsAuthenticated(false); + setIsUserMenuOpen(false); + } + }; + + const setModuleSubtab = (key: string, subtab: string) => { + try { + localStorage.setItem(key, subtab); + } catch (_) {} + }; + + const handleQuickAction = (action: string) => { + if (action === 'dashboard_task') { + setPendingQuickAction('dashboard_task'); + setActiveTab('dashboard'); + return; + } + if (action === 'inspection') { + setActiveTab('objects'); + if (!selectedBuilding) { + setTimeout(() => alert('Выберите дом для начала осмотра'), 100); + } + return; + } + if (action === 'task') { + setActiveTab('objects'); + if (!selectedBuilding) { + setTimeout(() => alert('Выберите дом для постановки задачи'), 100); + } + return; + } + if (action === 'request') { + setActiveTab('requests'); + return; + } + if (action === 'office_invoice') { + setActiveTab('office'); + setModuleSubtab('mkd_subTab_office', 'dashboard'); + return; + } + if (action === 'office_repair') { + setActiveTab('office'); + setModuleSubtab('mkd_subTab_office', 'repair'); + return; + } + if (action === 'office_article') { + setActiveTab('office'); + setModuleSubtab('mkd_subTab_office', 'knowledge'); + return; + } + if (action === 'office_document') { + setActiveTab('office'); + setModuleSubtab('mkd_subTab_office', 'docs'); + return; + } + if (action === 'office_equipment') { + setActiveTab('office'); + setModuleSubtab('mkd_subTab_office', 'facility'); + return; + } + if (action === 'office_order') { + setActiveTab('office'); + setModuleSubtab('mkd_subTab_office', 'supply'); + return; + } + if (action === 'office_meeting') { + setActiveTab('office'); + setModuleSubtab('mkd_subTab_office', 'meetings'); + return; + } + if (action === 'pr_feedback' || action === 'pr_incident') { + setActiveTab('pr'); + setModuleSubtab('mkd_subTab_pr', 'feedback'); + return; + } + if (action === 'pr_photo') { + setActiveTab('pr'); + setModuleSubtab('mkd_subTab_pr', 'photos'); + return; + } + if (action === 'pr_nps') { + setActiveTab('pr'); + setModuleSubtab('mkd_subTab_pr', 'nps'); + return; + } + if (action === 'invoice') { + setActiveTab('finance'); + setModuleSubtab('mkd_subTab_finance', 'invoices'); + return; + } + if (action === 'finance_calendar') { + setActiveTab('finance'); + setModuleSubtab('mkd_subTab_finance', 'calendar'); + return; + } + if (action === 'legal_contract') { + setActiveTab('legal'); + setModuleSubtab('mkd_subTab_legal', 'contracts'); + return; + } + if (action === 'legal_court') { + setActiveTab('legal'); + setModuleSubtab('mkd_subTab_legal', 'courts'); + return; + } + if (action === 'legal_debtor' || action === 'legal_action') { + setActiveTab('legal'); + setModuleSubtab('mkd_subTab_legal', 'preTrial'); + return; + } + if (action === 'legal_petition') { + setActiveTab('legal'); + setModuleSubtab('mkd_subTab_legal', 'debt'); + return; + } + if (action === 'development_oss') { + setActiveTab('development'); + setModuleSubtab('mkd_subTab_development', 'oss'); + return; + } + if (action === 'development_pipeline') { + setActiveTab('development'); + setModuleSubtab('mkd_subTab_development', 'pipeline'); + return; + } + if (action === 'development_marketing') { + setActiveTab('development'); + setModuleSubtab('mkd_subTab_development', 'marketing'); + return; + } + if (action === 'hire') { + setActiveTab('hr'); + setModuleSubtab('mkd_subTab_hr', 'hiring'); + return; + } + if (action === 'hr_vacancy') { + setActiveTab('hr'); + setModuleSubtab('mkd_subTab_hr', 'vacancies'); + return; + } + if (action === 'hr_employee') { + setActiveTab('hr'); + setModuleSubtab('mkd_subTab_hr', 'employees'); + return; + } + if (action === 'hr_calendar') { + setActiveTab('hr'); + setModuleSubtab('mkd_subTab_hr', 'calendar'); + return; + } + }; + + const renderContent = () => { + if (activeTab === 'objects') { + if (selectedBuilding) { + return ( + + ); + } + return ; + } + + if (activeTab === 'dashboard') { + const allowedDashboardBlocks = allowedSubsForSection(userPermissions ?? [], 'dashboard'); + return ( + setPendingQuickAction(null)} + openNewsId={dashboardOpenNewsId} + onCloseNews={() => setDashboardOpenNewsId(null)} + onNavigateToModule={(tabId) => setActiveTab(tabId)} + /> + ); + } + + if (activeTab === 'requests') { + return ; + } + + if (activeTab === 'pr') { + return ; + } + + if (activeTab === 'finance') { + return ( + { + setFinanceOpenInvoiceId(null); + setFinanceInvoicePrefill(null); + }} + allowedPermissions={userPermissions} + /> + ); + } + + if (activeTab === 'hr') { + return ; + } + + if (activeTab === 'office') { + return ; + } + + if (activeTab === 'legal') { + return ; + } + + if (activeTab === 'development') { + return ; + } + + if (activeTab === 'admin') { + return ; + } + + return ( +
+
+ 🏗️ +
+

Раздел в разработке

+

Функционал "{activeTab}" скоро появится в центре управления.

+
+ ); + }; + + return ( + +
+ + {/* Top Header */} +
+
+ + {/* Logo & Brand */} +
+
+ ЦУ +
+
+

Центр управления

+

жилым фондом

+
+
+ + {/* User & Actions */} +
+ +
+ + setIsNotificationPanelOpen(false)} + unreadCount={notificationUnreadCount} + onUnreadCountChange={setNotificationUnreadCount} + onNavigate={(entityType, entityId) => { + if (entityType === 'application') { + setActiveTab('requests'); + } else if (entityType === 'payment_invoice') { + setActiveTab('finance'); + setFinanceOpenInvoiceId(parseInt(entityId, 10)); + } else if (entityType === 'repair_request') { + setActiveTab('office'); + try { + localStorage.setItem('mkd_subTab_office', 'repair'); + } catch (_) {} + } else if (entityType === 'incident') { + setActiveTab('pr'); + try { + localStorage.setItem('mkd_subTab_pr', 'feedback'); + } catch (_) {} + } else if (entityType === 'pipeline') { + setActiveTab('development'); + } else if (entityType === 'pre_trial_work') { + setActiveTab('legal'); + try { + localStorage.setItem('mkd_subTab_legal', 'pre_trial'); + } catch (_) {} + } else if (entityType === 'training') { + setActiveTab('hr'); + try { + localStorage.setItem('mkd_subTab_hr', 'training'); + } catch (_) {} + } else if (entityType === 'company_news') { + setActiveTab('dashboard'); + const id = parseInt(entityId, 10); + if (!Number.isNaN(id)) setDashboardOpenNewsId(id); + } else if (entityType === 'outage') { + setActiveTab('requests'); + try { + localStorage.setItem('mkd_subTab_requests', 'outages'); + } catch (_) {} + } else if (entityType === 'oss') { + setActiveTab('development'); + try { + localStorage.setItem('mkd_subTab_development', 'oss'); + } catch (_) {} + } else if (entityType === 'legal_debtor') { + setActiveTab('legal'); + try { + localStorage.setItem('mkd_subTab_legal', 'debt'); + } catch (_) {} + } else if (entityType === 'pr_event') { + setActiveTab('pr'); + try { + localStorage.setItem('mkd_subTab_pr', 'events'); + } catch (_) {} + } else if (entityType === 'nps_survey') { + setActiveTab('pr'); + try { + localStorage.setItem('mkd_subTab_pr', 'nps'); + } catch (_) {} + } + }} + /> +
+ + {/* User Menu */} +
+ + + {/* Dropdown Menu */} + {isUserMenuOpen && ( + <> +
setIsUserMenuOpen(false)} + /> +
+ {/* User Info */} +
+
+ Profile +
+

{currentUser.name}

+

{ROLE_NAMES[currentUser.role]}

+
+
+
+ + {/* Профиль */} +
+ +
+ + {/* Logout */} +
+ +
+
+ + )} +
+
+
+
+ + {/* Main Content */} +
+ }> + {renderContent()} + +
+ + {/* Settings Modal */} + {isSettingsOpen && ( +
setIsSettingsOpen(false)}> +
e.stopPropagation()}> + {/* Header */} +
+
+
+

Настройки

+

Управление параметрами системы

+
+ +
+
+ + {/* Content: tabs on mobile, two columns on desktop */} +
+ {/* Mobile: horizontal tabs */} +
+ + +
+ {/* Desktop: left sidebar */} +
+
+ + +
+
+ {/* Right Content */} +
+
+ {activeSettingsTab === 'role' && ( +
+

Роль пользователя

+

Текущая роль определяет доступные разделы системы

+ +
+
+ + +
+
+
+ )} + + {activeSettingsTab === 'integrations' && ( +
+

Интеграции

+

Настройка подключений к внешним сервисам

+ + {/* Настройки Дома.АИ */} +
+
+
+ Д.АИ +
+
+
Дома.АИ
+

Интеграция с системой управления заявками

+
+
+ +
+
+ + setDomaAISettings({ ...domaAISettings, apiUrl: e.target.value })} + placeholder="https://your-domain.doma.ai/admin/api" + className="w-full px-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" + /> +
+ +
+ +