Files
mkd/components/SummaryDashboard.tsx

1828 lines
108 KiB
TypeScript
Raw Normal View History

2026-02-04 00:17:04 +05:00
import React, { useState, useEffect, useMemo } from 'react';
import {
Briefcase,
Megaphone,
Wrench,
Banknote,
Scale,
TrendingUp,
TrendingDown,
UsersRound,
Calendar,
CalendarDays,
Pencil,
X,
Target,
Share2,
Send,
MessageSquare,
CheckCircle2,
User as UserIcon,
Clock,
Plus,
UserPlus,
Eye,
Send as SendIcon,
Newspaper,
ArrowRight,
LayoutGrid,
Inbox,
AlertTriangle,
Camera,
Package,
Gauge,
FileText,
PartyPopper,
ClipboardList
} from 'lucide-react';
import { StrategicGoal, Department, UserRole, DomaApplication, User, BuildingTask, CompanyNews } from '../types';
import { MOCK_STRATEGIC_GOALS, CURRENT_USER_MOCK } from '../constants';
import { backendApi } from '../services/apiClient';
import { DashboardTaskModal } from './DashboardTaskModal';
const DASHBOARD_TASKS_KEY = 'mkd_dashboard_tasks';
/** Роль текущего пользователя в задаче */
export type TaskMyRole = 'executor' | 'co_executor' | 'assigner' | 'observer';
/** Мок задач для сводки (если нет сохранённых) */
function getMockDashboardTasks(currentUserId: string): BuildingTask[] {
const now = new Date().toISOString();
const d = (days: number) => new Date(Date.now() + days * 86400000).toISOString().slice(0, 10);
return [
{ id: 'dt-1', title: 'Проверка стояков отопления', deadline: d(5), status: 'in_progress', priority: 'high', createdAt: now, updatedAt: now, buildingId: 'b-1', buildingAddress: 'ул. Мира, 10', assignedTo: currentUserId, assignedToName: 'Я', createdBy: 'u2', createdByName: 'Смирнов А.В.', requirePhotoReport: false, comments: [{ id: 'c1', authorName: 'Смирнов А.В.', text: 'Датчики заказаны, ждём поставку.', createdAt: now }] },
{ id: 'dt-2', title: 'Подготовка отчёта по дому', deadline: d(3), status: 'new', priority: 'medium', createdAt: now, updatedAt: now, buildingId: 'b-1', buildingAddress: 'ул. Мира, 10', assignedTo: 'u2', assignedToName: 'Смирнов А.В.', coAssignees: [currentUserId], coAssigneesNames: ['Я'], createdBy: 'u1', createdByName: 'Петров А.В.', requirePhotoReport: false },
{ id: 'dt-3', title: 'Согласование сметы на ремонт', deadline: d(7), status: 'new', priority: 'urgent', createdAt: now, updatedAt: now, buildingId: 'b-2', buildingAddress: 'пр. Ленина, 5', assignedTo: 'u3', assignedToName: 'Кузнецов В.В.', createdBy: currentUserId, createdByName: 'Я', requirePhotoReport: false },
{ id: 'dt-4', title: 'Контроль качества уборки', deadline: d(1), status: 'done', priority: 'medium', createdAt: now, updatedAt: now, buildingId: 'b-1', buildingAddress: 'ул. Мира, 10', assignedTo: 'u2', assignedToName: 'Смирнов А.В.', observers: [currentUserId], observersNames: ['Я'], createdBy: 'u2', createdByName: 'Смирнов А.В.', requirePhotoReport: false },
];
}
function loadDashboardTasks(currentUserId: string): BuildingTask[] {
try {
const raw = localStorage.getItem(DASHBOARD_TASKS_KEY);
if (!raw) return getMockDashboardTasks(currentUserId);
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed) || parsed.length === 0) return getMockDashboardTasks(currentUserId);
return parsed as BuildingTask[];
} catch {
return getMockDashboardTasks(currentUserId);
}
}
function saveDashboardTasks(tasks: BuildingTask[]): void {
try {
localStorage.setItem(DASHBOARD_TASKS_KEY, JSON.stringify(tasks));
} catch (e) {
console.error('[Dashboard] failed to persist tasks', e);
}
}
function getTaskMyRole(task: BuildingTask, currentUserId: string): TaskMyRole | null {
if (task.assignedTo === currentUserId) return 'executor';
if (task.coAssignees?.includes(currentUserId)) return 'co_executor';
if (task.createdBy === currentUserId) return 'assigner';
if (task.observers?.includes(currentUserId)) return 'observer';
return null;
}
// ====================================================================================
// Strategic Goals Components
// ====================================================================================
const DEPT_CONFIG: Record<Department, { icon: React.ElementType; color: string; }> = {
production: { icon: Wrench, color: 'text-amber-500' },
pr: { icon: Megaphone, color: 'text-indigo-500' },
finance: { icon: Banknote, color: 'text-emerald-500' },
development: { icon: Briefcase, color: 'text-sky-500' },
legal: { icon: Scale, color: 'text-slate-600' },
hr: { icon: UsersRound, color: 'text-violet-500' },
};
const GoalCard: React.FC<{ goal: StrategicGoal }> = ({ goal }) => {
const { icon: Icon, color } = DEPT_CONFIG[goal.department];
const lowerIsBetter = ['production', 'hr'].includes(goal.department);
let progress = 0;
if (lowerIsBetter) {
const startValue = goal.targetValue * 2;
progress = Math.max(0, (startValue - goal.currentValue) / (startValue - goal.targetValue)) * 100;
} else {
progress = (goal.currentValue / goal.targetValue) * 100;
}
progress = Math.min(100, Math.max(0, progress));
const formatValue = (val: number, unit: string) => {
if (unit === '₽') return `${(val / 1000000).toFixed(1)}M ₽`;
return `${val}${unit}`;
};
return (
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
<div>
<div className="flex items-center gap-3 mb-3">
<div className={`p-2 bg-slate-100 rounded-lg ${color}`}>
<Icon className="w-5 h-5" />
</div>
<h4 className="font-bold text-slate-700">{goal.title}</h4>
</div>
<p className="text-sm text-slate-600 mb-4">{goal.description}</p>
</div>
<div>
<div className="flex justify-between items-end mb-1">
<span className="text-xs text-slate-500">Прогресс</span>
<span className="text-sm font-bold text-slate-800">
{formatValue(goal.currentValue, goal.unit)} / <span className={color}>{formatValue(goal.targetValue, goal.unit)}</span>
</span>
</div>
<div className="w-full bg-slate-200 rounded-full h-2.5">
<div className={`${color.replace('text-', 'bg-')} h-2.5 rounded-full`} style={{ width: `${progress}%` }}></div>
</div>
<div className="text-xs text-slate-400 mt-2 flex items-center gap-1.5">
<Calendar className="w-3 h-3"/>
<span>Срок: {goal.deadline}</span>
</div>
</div>
</div>
);
};
// ====================================================================================
// Operational Panel Components
// ====================================================================================
/** Блок дашборда, по которому можно открыть продвинутую статистику и перейти в модуль */
const DASHBOARD_BLOCK_TO_MODULE: Record<string, { tabId: string; label: string }> = {
production: { tabId: 'objects', label: 'Участки' },
pr: { tabId: 'pr', label: 'PR и NPS' },
finance: { tabId: 'finance', label: 'Финансы' },
development: { tabId: 'development', label: 'Развитие' },
legal: { tabId: 'legal', label: 'Юр. отдел' },
hr: { tabId: 'hr', label: 'Кадры' },
news: { tabId: 'office', label: 'Офис' },
};
/** Модалка продвинутой статистики по отделу с кнопкой «Перейти в модуль» */
const DepartmentStatsModal: React.FC<{
title: string;
blockId: string;
onClose: () => void;
onGoToModule?: (tabId: string) => void;
children: React.ReactNode;
}> = ({ title, blockId, onClose, onGoToModule, children }) => {
const moduleInfo = DASHBOARD_BLOCK_TO_MODULE[blockId];
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 pb-24 md:pb-28 bg-black/50 backdrop-blur-sm animate-fade-in" onClick={onClose}>
<div
className="bg-white rounded-2xl shadow-xl border border-slate-200 w-full max-w-5xl max-h-[calc(100vh-7rem)] md:max-h-[calc(100vh-8rem)] overflow-hidden flex flex-col animate-fade-in"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between gap-4 p-4 border-b border-slate-200 bg-slate-50/80">
<h3 className="text-lg font-bold text-slate-800">{title} продвинутая статистика</h3>
<div className="flex items-center gap-3">
{moduleInfo && onGoToModule && (
<button
type="button"
onClick={() => {
onGoToModule(moduleInfo.tabId);
onClose();
}}
className="flex items-center gap-2 px-4 py-2.5 bg-primary-600 text-white rounded-xl font-bold text-sm hover:bg-primary-700 transition-all shadow-sm"
>
Перейти в модуль
<ArrowRight className="w-4 h-4" />
</button>
)}
<button
type="button"
onClick={onClose}
className="p-2 text-slate-400 hover:text-slate-600 rounded-lg transition-colors"
aria-label="Закрыть"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
<div className="p-6 overflow-y-auto flex-1">
{children}
</div>
</div>
</div>
);
};
const DashboardCard: React.FC<{
title: string;
icon: React.ElementType;
iconColor: string;
children: React.ReactNode;
/** При указании карточка кликабельна — открывается продвинутая статистика по отделу */
onClick?: () => void;
}> = ({ title, icon: Icon, iconColor, children, onClick }) => {
const Wrapper = onClick ? 'button' : 'div';
const wrapperProps = onClick
? { type: 'button' as const, onClick, className: 'w-full text-left bg-white p-5 rounded-xl border border-slate-200 shadow-sm flex flex-col gap-4 hover:shadow-md transition-shadow cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500/50' }
: { className: 'bg-white p-5 rounded-xl border border-slate-200 shadow-sm flex flex-col gap-4 hover:shadow-md transition-shadow' };
return (
<Wrapper {...wrapperProps}>
<div className="flex items-center gap-3">
<div className={`p-2 bg-slate-50 rounded-lg ${iconColor}`}>
<Icon className="w-5 h-5" />
</div>
<h3 className="font-bold text-slate-700">{title}</h3>
</div>
<div className="space-y-4">
{children}
</div>
</Wrapper>
);
};
const Stat: React.FC<{
value: string;
label: string;
trend?: 'up' | 'down';
}> = ({ value, label, trend }) => {
const trendColor = trend === 'up' ? 'text-emerald-500' : 'text-red-500';
const TrendIcon = trend === 'up' ? TrendingUp : TrendingDown;
return (
<div className="flex justify-between items-center">
<div>
<p className="text-2xl font-black text-slate-800">{value}</p>
<p className="text-xs text-slate-500">{label}</p>
</div>
{trend && <TrendIcon className={`w-6 h-6 ${trendColor}`} />}
</div>
);
};
const ProgressBar: React.FC<{ value: number; label: string }> = ({ value, label }) => (
<div>
<div className="flex justify-between items-center mb-1">
<p className="text-xs text-slate-500 font-bold">{label}</p>
<p className="text-sm font-bold text-slate-700">{value}%</p>
</div>
<div className="w-full bg-slate-200 rounded-full h-2">
<div className="bg-sky-500 h-2 rounded-full" style={{ width: `${value}%` }}></div>
</div>
</div>
);
const GaugeChart: React.FC<{ value: number; label: string }> = ({ value, label }) => {
const color = value > 75 ? 'bg-emerald-500' : value > 50 ? 'bg-blue-500' : value > 25 ? 'bg-amber-500' : 'bg-red-500';
return (
<div>
<div className="flex justify-between items-baseline">
<p className="text-xs text-slate-500">{label}</p>
<p className="text-2xl font-black text-slate-800">{value}%</p>
</div>
<div className="w-full bg-slate-200 rounded-full h-2 mt-2">
<div className={`${color} h-2 rounded-full`} style={{ width: `${value}%` }}></div>
</div>
</div>
);
};
const getPriorityColor = (p: BuildingTask['priority']) => {
switch (p) { case 'urgent': return 'bg-red-50 text-red-600'; case 'high': return 'bg-amber-50 text-amber-600'; case 'medium': return 'bg-blue-50 text-blue-600'; default: return 'bg-slate-100 text-slate-600'; }
};
const getPriorityLabel = (p: BuildingTask['priority']) => {
switch (p) { case 'urgent': return 'Срочно'; case 'high': return 'Высокий'; case 'medium': return 'Средний'; case 'low': return 'Низкий'; default: return p; }
};
const formatTaskDate = (dateStr: string) => {
try { const d = new Date(dateStr); return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }); } catch { return '—'; }
};
const ROLE_LABELS: Record<TaskMyRole, string> = { executor: 'Исполнитель', co_executor: 'Соисполнитель', assigner: 'Постановщик', observer: 'Наблюдатель' };
const DashboardTaskCard: React.FC<{
task: BuildingTask;
myRole: TaskMyRole;
onToggleDone: () => void;
onOpen: () => void;
}> = ({ task, myRole, onToggleDone, onOpen }) => (
<div
onClick={onOpen}
className="flex items-start gap-3 p-3 hover:bg-slate-50 rounded-xl border border-slate-100 transition-colors cursor-pointer"
>
<button
type="button"
onClick={(e) => { e.stopPropagation(); onToggleDone(); }}
className={`mt-0.5 w-5 h-5 rounded-full border-2 flex-shrink-0 flex items-center justify-center transition-all ${
task.status === 'done' ? 'bg-emerald-500 border-emerald-500' : 'border-slate-300 hover:border-primary-400'
}`}
title={task.status === 'done' ? 'Отметить невыполненной' : 'Отметить выполненной'}
>
{task.status === 'done' && <svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /></svg>}
</button>
<div className="flex-1 min-w-0">
<p className={`text-sm font-bold truncate ${task.status === 'done' ? 'text-slate-400 line-through' : 'text-slate-800'}`}>{task.title}</p>
<div className="flex flex-wrap items-center gap-2 mt-1.5">
<span className="text-[10px] text-slate-400 font-bold flex items-center gap-1"><Calendar className="w-3 h-3" /> {formatTaskDate(task.deadline)}</span>
<span className={`px-1.5 py-0.5 rounded text-[9px] font-black uppercase ${getPriorityColor(task.priority)}`}>{getPriorityLabel(task.priority)}</span>
{task.buildingAddress && <span className="text-[10px] text-slate-500 truncate max-w-[120px]" title={task.buildingAddress}>{task.buildingAddress}</span>}
<span className="text-[9px] font-bold text-primary-600 bg-primary-50 px-1.5 py-0.5 rounded">{ROLE_LABELS[myRole]}</span>
{(task.comments?.length ?? 0) > 0 && (
<span className="text-[10px] text-slate-500 font-bold flex items-center gap-1" title={task.comments!.map(c => `${c.authorName}: ${c.text}`).join('\n')}>
<MessageSquare className="w-3 h-3" /> {task.comments!.length} коммент.
</span>
)}
</div>
</div>
</div>
);
// ====================================================================================
// Main Dashboard Component
// ====================================================================================
export const SummaryDashboard: React.FC<{
currentUserRole: UserRole;
currentUser?: User;
/** Какие блоки сводки показывать: 'all' или список id (goals, production, pr, finance, development, legal, hr, tasks, news) */
allowedDashboardBlocks?: string[] | 'all';
initialOpenTaskModal?: boolean;
onQuickActionHandled?: () => void;
/** Открыть модалку просмотра новости по id (из уведомления) */
openNewsId?: number | null;
onCloseNews?: () => void;
/** Переход в модуль (HR, PR, Финансы и т.д.) из продвинутой статистики */
onNavigateToModule?: (tabId: string) => void;
}> = ({ currentUserRole, currentUser: currentUserProp, allowedDashboardBlocks = 'all', initialOpenTaskModal, onQuickActionHandled, openNewsId, onCloseNews, onNavigateToModule }) => {
const currentUser = currentUserProp ?? CURRENT_USER_MOCK;
const [isGoalsModalOpen, setIsGoalsModalOpen] = useState(false);
/** Открытая модалка продвинутой статистики по отделу (production, pr, finance, development, legal, hr, news) */
const [statsModalDepartment, setStatsModalDepartment] = useState<string | null>(null);
const [tasksTab, setTasksTab] = useState<TaskMyRole>('executor');
const [dashboardTasks, setDashboardTasks] = useState<BuildingTask[]>(() => loadDashboardTasks(currentUser.id));
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
const [editingTask, setEditingTask] = useState<BuildingTask | null>(null);
const [applications, setApplications] = useState<DomaApplication[]>([]);
const [appsLoading, setAppsLoading] = useState(false);
const [appsError, setAppsError] = useState<string | null>(null);
const [productionMetrics, setProductionMetrics] = useState<{
buildingCondition: { value: number; label: string; condition: string; totalBuildings: number; buildingsWithWear: number };
workSchedule: { onTime: number; late: number; overdue: number; onTimeRate: number; lateRate: number };
slaRating: number;
} | null>(null);
const [metricsLoading, setMetricsLoading] = useState(false);
/** Полный дашборд производства для модалки продвинутой статистики */
const [productionDashboard, setProductionDashboard] = useState<Awaited<ReturnType<typeof backendApi.getProductionDashboard>>['data'] | null>(null);
const [productionDashboardLoading, setProductionDashboardLoading] = useState(false);
const [companyNews, setCompanyNews] = useState<CompanyNews[]>([]);
const [companyNewsLoading, setCompanyNewsLoading] = useState(false);
const [isSuggestNewsModalOpen, setIsSuggestNewsModalOpen] = useState(false);
const [viewNewsItem, setViewNewsItem] = useState<CompanyNews | null>(null);
const [viewNewsLoading, setViewNewsLoading] = useState(false);
const [smmSummary, setSmmSummary] = useState<{ total: number; byChannel: Array<{ id: number; name: string; type: string; subscribersCount: number }> } | null>(null);
const [smmSummaryLoading, setSmmSummaryLoading] = useState(false);
/** Дашборд PR для карточки и модалки продвинутой статистики */
const [prDashboard, setPrDashboard] = useState<Awaited<ReturnType<typeof backendApi.getPRDashboard>>['data'] | null>(null);
const [prDashboardLoading, setPrDashboardLoading] = useState(false);
/** Дашборд офиса для карточки и модалки продвинутой статистики */
const [officeDashboard, setOfficeDashboard] = useState<Awaited<ReturnType<typeof backendApi.getOfficeDashboard>>['data'] | null>(null);
const [officeDashboardLoading, setOfficeDashboardLoading] = useState(false);
const canShowBlock = (blockId: string) =>
allowedDashboardBlocks === 'all' || (Array.isArray(allowedDashboardBlocks) && allowedDashboardBlocks.includes(blockId));
const canSee = (dept: Department) => {
if (currentUserRole === 'DIRECTOR') return true;
if (currentUserRole === 'ENGINEER') return dept === 'production' || dept === 'development';
if (currentUserRole === 'MASTER') return dept === 'production';
if (currentUserRole === 'LAWYER') return dept === 'legal';
if (currentUserRole === 'FINANCIER') return dept === 'finance';
if (currentUserRole === 'HR_MANAGER') return dept === 'hr';
if (currentUserRole === 'PR_MANAGER') return dept === 'pr';
return false;
};
/** Показывать карточку дашборда: при тонкой настройке прав — только canShowBlock, при доступе «всё» — ещё и canSee по роли */
const showDashboardCard = (blockId: string, dept?: Department) =>
canShowBlock(blockId) && (allowedDashboardBlocks === 'all' ? (dept == null || canSee(dept)) : true);
const filteredGoals =
allowedDashboardBlocks === 'all'
? MOCK_STRATEGIC_GOALS.filter((g) => canSee(g.department))
: canShowBlock('goals')
? MOCK_STRATEGIC_GOALS
: [];
// Быстрое действие «Добавить задачу» — открыть модалку при переходе со сводки
useEffect(() => {
if (initialOpenTaskModal) {
setIsTaskModalOpen(true);
onQuickActionHandled?.();
}
}, [initialOpenTaskModal, onQuickActionHandled]);
// Сохраняем задачи сводки в localStorage при любом изменении
useEffect(() => {
saveDashboardTasks(dashboardTasks);
}, [dashboardTasks]);
// Загружаем метрики производства (для карточки на сводке)
useEffect(() => {
let cancelled = false;
const loadMetrics = async () => {
setMetricsLoading(true);
try {
const response = await backendApi.getProductionMetrics();
if (!cancelled && response.success) {
setProductionMetrics(response.data);
}
} catch (err: any) {
console.error('[SummaryDashboard] Ошибка загрузки метрик производства:', err);
} finally {
if (!cancelled) {
setMetricsLoading(false);
}
}
};
loadMetrics();
return () => {
cancelled = true;
};
}, []);
// Загружаем полный дашборд производства при открытии модалки продвинутой статистики
useEffect(() => {
if (statsModalDepartment !== 'production') {
setProductionDashboard(null);
return;
}
let cancelled = false;
setProductionDashboardLoading(true);
setProductionDashboard(null);
backendApi
.getProductionDashboard()
.then((res) => {
if (!cancelled && res.success && res.data) setProductionDashboard(res.data);
})
.catch((err) => {
if (!cancelled) console.error('[SummaryDashboard] Ошибка загрузки дашборда производства:', err);
})
.finally(() => {
if (!cancelled) setProductionDashboardLoading(false);
});
return () => { cancelled = true; };
}, [statsModalDepartment]);
// Загрузка дашборда PR (NPS, CSAT, SMM и др.) для карточки и модалки
useEffect(() => {
if (!showDashboardCard('pr', 'pr')) return;
let cancelled = false;
setPrDashboardLoading(true);
setPrDashboard(null);
backendApi
.getPRDashboard()
.then((res) => {
if (!cancelled && res.success && res.data) setPrDashboard(res.data);
})
.catch((err) => {
if (!cancelled) console.error('[SummaryDashboard] Ошибка загрузки дашборда PR:', err);
})
.finally(() => {
if (!cancelled) setPrDashboardLoading(false);
});
return () => { cancelled = true; };
}, [allowedDashboardBlocks, currentUserRole]);
// Загрузка сводки SMM (подписчики по каналам) для блока PR — fallback, если дашборд не загружен
useEffect(() => {
if (!showDashboardCard('pr', 'pr')) return;
let cancelled = false;
setSmmSummaryLoading(true);
backendApi
.getSMMChannelsSummary()
.then((data) => {
if (!cancelled && data) setSmmSummary({ total: data.total ?? 0, byChannel: data.byChannel ?? [] });
})
.catch(() => {
if (!cancelled) setSmmSummary(null);
})
.finally(() => {
if (!cancelled) setSmmSummaryLoading(false);
});
return () => { cancelled = true; };
}, [allowedDashboardBlocks, currentUserRole]);
// Загрузка дашборда офиса для карточки и модалки (блок office или news)
useEffect(() => {
if (!showDashboardCard('office') && !showDashboardCard('news')) return;
let cancelled = false;
setOfficeDashboardLoading(true);
setOfficeDashboard(null);
backendApi
.getOfficeDashboard()
.then((res) => {
if (!cancelled && res.success && res.data) setOfficeDashboard(res.data);
})
.catch((err) => {
if (!cancelled) console.error('[SummaryDashboard] Ошибка загрузки дашборда офиса:', err);
})
.finally(() => {
if (!cancelled) setOfficeDashboardLoading(false);
});
return () => { cancelled = true; };
}, [allowedDashboardBlocks]);
// Загрузка опубликованных новостей компании (блок «Новости компании»)
useEffect(() => {
if (!canShowBlock('news')) return;
let cancelled = false;
const load = async () => {
setCompanyNewsLoading(true);
try {
const list = await backendApi.getCompanyNews({ status: 'published', limit: 5 });
if (!cancelled && Array.isArray(list)) setCompanyNews(list);
} catch (err) {
if (!cancelled) setCompanyNews([]);
} finally {
if (!cancelled) setCompanyNewsLoading(false);
}
};
load();
return () => { cancelled = true; };
}, [allowedDashboardBlocks]);
// Открыть модалку просмотра новости по openNewsId (из уведомления)
useEffect(() => {
if (openNewsId == null) {
setViewNewsItem(null);
return;
}
let cancelled = false;
setViewNewsLoading(true);
setViewNewsItem(null);
backendApi
.getCompanyNewsById(openNewsId)
.then((item) => {
if (!cancelled) {
setViewNewsItem(item);
}
})
.catch(() => {
if (!cancelled) setViewNewsItem(null);
})
.finally(() => {
if (!cancelled) setViewNewsLoading(false);
});
return () => { cancelled = true; };
}, [openNewsId]);
const appsStats = useMemo(() => {
const stats = {
total: applications.length,
new: 0,
inProgress: 0,
deferred: 0,
done: 0,
canceled: 0,
overdue: 0,
overduePercent: 0,
completionRate: 0,
active: 0,
};
if (applications.length === 0) {
return stats;
}
stats.new = applications.filter(a => a.status === 'new').length;
stats.inProgress = applications.filter(a => a.status === 'in_progress').length;
stats.deferred = applications.filter(a => a.status === 'deferred').length;
stats.done = applications.filter(a => a.status === 'done').length;
stats.canceled = applications.filter(a => a.status === 'canceled').length;
// Используем поле isOverdue из БД, если есть, иначе вычисляем
stats.overdue = applications.filter(a =>
a.isOverdue !== undefined
? a.isOverdue
: (a.status !== 'done' && a.status !== 'canceled' && new Date(a.deadlineAt) < new Date())
).length;
stats.active = stats.new + stats.inProgress + stats.deferred;
if (stats.total > 0) {
stats.overduePercent = Math.round((stats.overdue / stats.total) * 100);
}
if (stats.done + stats.active > 0) {
stats.completionRate = Math.round((stats.done / (stats.done + stats.active)) * 100);
}
return stats;
}, [applications]);
return (
<div className="animate-fade-in pb-20 space-y-8">
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
<div>
<h2 className="text-xl font-bold text-slate-800">Сводка</h2>
<p className="text-sm text-slate-500">Ключевые показатели для вашей роли.</p>
</div>
{/* 'Set Goals' Button - prominent placement */}
{canShowBlock('goals') && (
<button
onClick={() => setIsGoalsModalOpen(true)}
className="flex items-center justify-center gap-2 px-6 py-3 bg-white border-2 border-primary-600 text-primary-700 rounded-2xl font-bold text-sm hover:bg-primary-50 transition-all shadow-sm active:scale-95 group"
>
<Target className="w-5 h-5 text-primary-600 group-hover:scale-110 transition-transform" />
Стратегические цели
</button>
)}
</div>
{/* KPI Dashboards Section - Main focus */}
<div className="space-y-6">
<div className="flex items-center gap-3">
<h3 className="text-lg font-bold text-slate-800">Оперативные Панели</h3>
<div className="h-px flex-1 bg-slate-200"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{showDashboardCard('production', 'production') && (
<DashboardCard title="Производство" icon={Wrench} iconColor="text-amber-500" onClick={() => setStatsModalDepartment('production')}>
{metricsLoading ? (
<div className="text-center py-4 text-slate-400 text-sm">Загрузка...</div>
) : productionMetrics ? (
<>
{/* Состояние домов */}
<div className="space-y-1">
<div className="flex justify-between items-center">
<span className="text-xs text-slate-600 font-bold">Состояние домов</span>
<span className={`text-sm font-black ${
productionMetrics.buildingCondition.condition === 'excellent' ? 'text-emerald-600' :
productionMetrics.buildingCondition.condition === 'good' ? 'text-blue-600' :
productionMetrics.buildingCondition.condition === 'fair' ? 'text-amber-600' :
'text-red-600'
}`}>
{productionMetrics.buildingCondition.label}
</span>
</div>
<div className="text-[10px] text-slate-400">
Средний износ: {productionMetrics.buildingCondition.value > 0 ? `${productionMetrics.buildingCondition.value.toFixed(1)}%` : 'Н/Д'}
({productionMetrics.buildingCondition.buildingsWithWear} из {productionMetrics.buildingCondition.totalBuildings} домов)
</div>
</div>
{/* График работ */}
<div className="space-y-2 pt-2 border-t border-slate-100">
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600 font-bold">График работ</span>
</div>
<div className="space-y-1.5">
<div className="flex justify-between items-center">
<span className="text-[10px] text-slate-500">В срок:</span>
<span className="text-sm font-black text-emerald-600">{productionMetrics.workSchedule.onTimeRate}%</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[10px] text-slate-500">С опозданием:</span>
<span className="text-sm font-black text-red-600">{productionMetrics.workSchedule.lateRate}%</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[10px] text-slate-500">Просрочено:</span>
<span className="text-sm font-black text-amber-600">{productionMetrics.workSchedule.overdue}</span>
</div>
</div>
</div>
{/* Рейтинг SLA */}
<div className="pt-2 border-t border-slate-100">
<GaugeChart
value={productionMetrics.slaRating}
label={`Рейтинг SLA`}
/>
<div className="text-[10px] text-slate-400 mt-1 text-center">
{productionMetrics.workSchedule.onTime} из {productionMetrics.workSchedule.onTime + productionMetrics.workSchedule.late} выполнено в срок
</div>
</div>
</>
) : (
<div className="text-center py-4 text-slate-400 text-sm">Нет данных</div>
)}
</DashboardCard>
)}
{showDashboardCard('pr', 'pr') && (
<DashboardCard title="PR и Клиентский Опыт" icon={Megaphone} iconColor="text-indigo-500" onClick={() => setStatsModalDepartment('pr')}>
{prDashboardLoading && !prDashboard ? (
<div className="text-center py-4 text-slate-400 text-sm">Загрузка...</div>
) : (
<>
<GaugeChart value={prDashboard?.npsCompany ?? 0} label="Общий NPS по компании" />
<Stat value={`${prDashboard?.csatPercent ?? 0}%`} label="Удовлетворенность (CSAT)" />
<div className="pt-4 border-t border-slate-100 space-y-2">
<div className="flex items-center gap-2">
<Share2 className="w-4 h-4 text-indigo-500" />
<span className="text-xs font-bold text-slate-600">SMM подписчики</span>
</div>
{(prDashboardLoading && !prDashboard) ? (
<p className="text-xs text-slate-400">Загрузка...</p>
) : (() => {
const smm = prDashboard?.smm ?? smmSummary;
return smm && (smm.byChannel?.length > 0 || (smm.total ?? 0) > 0) ? (
<>
<div className="flex justify-between items-baseline">
<p className="text-2xl font-black text-slate-800">{(smm.total ?? 0).toLocaleString('ru-RU')}</p>
<span className="text-[10px] text-slate-400 font-bold uppercase">всего</span>
</div>
{smm.byChannel && smm.byChannel.length > 0 ? (
<div className="flex flex-wrap gap-x-3 gap-y-1 text-[10px]">
{smm.byChannel.map((c: { id: number; name: string; type: string; subscribersCount: number }) => (
<span key={c.id} className="flex items-center gap-1">
{c.type === 'tg' && <Send className="w-3 h-3 text-sky-600" />}
{c.type === 'vk' && <Share2 className="w-3 h-3 text-blue-600" />}
{c.type === 'wa' && <MessageSquare className="w-3 h-3 text-emerald-600" />}
<span className={c.type === 'tg' ? 'text-sky-600' : c.type === 'vk' ? 'text-blue-600' : c.type === 'wa' ? 'text-emerald-600' : 'text-slate-600'}>
{c.type === 'tg' ? 'TG' : c.type === 'vk' ? 'VK' : c.type === 'wa' ? 'WA' : c.name} {(c.subscribersCount ?? 0).toLocaleString('ru-RU')}
</span>
</span>
))}
</div>
) : (
<p className="text-[10px] text-slate-400">Добавьте каналы в модуле PR SMM</p>
)}
</>
) : (
<p className="text-[10px] text-slate-400">Добавьте каналы в модуле PR SMM</p>
);
})()}
</div>
</>
)}
</DashboardCard>
)}
{showDashboardCard('finance', 'finance') && (
<DashboardCard title="Финансы" icon={Banknote} iconColor="text-emerald-500" onClick={() => setStatsModalDepartment('finance')}>
<Stat value="1.2M ₽" label="Чистая прибыль (мес)" trend="up" />
<Stat value="8.5M ₽" label="Общая дебиторка" />
<div className="p-3 bg-slate-50 rounded-lg text-center">
<TrendingUp className="w-10 h-10 text-emerald-300 mx-auto opacity-50" />
<p className="text-[10px] text-slate-400 mt-1 uppercase font-bold tracking-widest">Тренд прибыли</p>
</div>
</DashboardCard>
)}
{showDashboardCard('development', 'development') && (
<DashboardCard title="Отдел Развития" icon={Briefcase} iconColor="text-sky-500" onClick={() => setStatsModalDepartment('development')}>
<Stat value="+3" label="Новых дома (квартал)" />
<Stat value="12" label="Домов в воронке продаж" />
<ProgressBar value={25} label="Конверсия" />
</DashboardCard>
)}
{showDashboardCard('legal', 'legal') && (
<DashboardCard title="Юридический отдел" icon={Scale} iconColor="text-slate-600" onClick={() => setStatsModalDepartment('legal')}>
<Stat value="2.1M ₽" label="Взыскано за квартал" trend="up"/>
<Stat value="42" label="Активных судебных дел" />
<ProgressBar value={85} label="Процент выигранных дел" />
</DashboardCard>
)}
{showDashboardCard('hr', 'hr') && (
<DashboardCard title="Отдел Кадров (HR)" icon={UsersRound} iconColor="text-violet-500" onClick={() => setStatsModalDepartment('hr')}>
<Stat value="24" label="Всего сотрудников" />
<Stat value="2" label="Кандидатов в воронке" />
<ProgressBar value={12} label="Текучесть кадров (год)" />
</DashboardCard>
)}
{(showDashboardCard('office') || showDashboardCard('news')) && (allowedDashboardBlocks === 'all' ? currentUserRole !== 'MASTER' : true) && (
<DashboardCard title="Офис" icon={LayoutGrid} iconColor="text-slate-600" onClick={() => setStatsModalDepartment('news')}>
{officeDashboardLoading && !officeDashboard ? (
<div className="text-center py-4 text-slate-400 text-sm">Загрузка...</div>
) : officeDashboard ? (
<>
<Stat value={String(officeDashboard.companyNews.publishedThisMonth)} label="Новостей за месяц" />
<Stat value={String(officeDashboard.repairRequests.open + officeDashboard.repairRequests.in_progress)} label="Заявок на ремонт (активных)" />
<div className="pt-2 border-t border-slate-100 space-y-1">
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600 font-bold">Документы</span>
<span className="text-slate-800 font-black">{officeDashboard.documents.total}</span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-500">Заявки на закупку</span>
<span className="text-slate-700 font-bold">{officeDashboard.supplyRequests.total}</span>
</div>
</div>
</>
) : (
<div className="text-center py-4 text-slate-400 text-sm">Нет данных</div>
)}
</DashboardCard>
)}
</div>
</div>
{/* Новости компании — чтение и предложение новости */}
{canShowBlock('news') && (
<div className="space-y-6">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<h3 className="text-lg font-bold text-slate-800">Новости компании</h3>
<div className="h-px flex-1 bg-slate-200"></div>
</div>
<button
type="button"
onClick={() => setIsSuggestNewsModalOpen(true)}
className="flex items-center justify-center gap-2 px-4 py-2.5 bg-primary-600 text-white rounded-xl font-bold text-sm hover:bg-primary-700 transition-all shadow-sm active:scale-95"
>
<Newspaper className="w-4 h-4" /> Предложить новость
</button>
</div>
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
{companyNewsLoading ? (
<div className="p-6 text-center text-slate-400 text-sm">Загрузка...</div>
) : companyNews.length === 0 ? (
<div className="p-6 text-center text-slate-400 text-sm">Нет опубликованных новостей</div>
) : (
<ul className="divide-y divide-slate-100">
{companyNews.map((n) => (
<li key={n.id} className="p-4 hover:bg-slate-50/50 transition-colors">
<div className="flex justify-between items-start gap-2">
<div className="min-w-0 flex-1">
<p className="font-bold text-slate-800 truncate">{n.title}</p>
<p className="text-xs text-slate-500 mt-0.5">
{n.publishedAt ? new Date(n.publishedAt).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric' }) : new Date(n.createdAt).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric' })}
{n.createdByName && ` · ${n.createdByName}`}
</p>
{n.body && <p className="text-sm text-slate-600 mt-1 line-clamp-2">{n.body}</p>}
</div>
</div>
</li>
))}
</ul>
)}
</div>
</div>
)}
{/* Мои задачи — список по ролям (исполнитель / соисполнитель / постановщик / наблюдатель) */}
{canShowBlock('tasks') && (
<div className="space-y-6">
<div className="flex items-center gap-3">
<h3 className="text-lg font-bold text-slate-800">Мои задачи</h3>
<div className="h-px flex-1 bg-slate-200"></div>
</div>
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="flex p-1 bg-slate-100/80 border-b border-slate-200 overflow-x-auto no-scrollbar gap-1">
{([
{ id: 'executor' as TaskMyRole, label: 'Я исполнитель', icon: UserIcon },
{ id: 'co_executor' as TaskMyRole, label: 'Я соисполнитель', icon: UserPlus },
{ id: 'assigner' as TaskMyRole, label: 'Я постановщик', icon: SendIcon },
{ id: 'observer' as TaskMyRole, label: 'Я наблюдатель', icon: Eye },
]).map(({ id, label, icon: Icon }) => (
<button
key={id}
onClick={() => setTasksTab(id)}
className={`flex-shrink-0 min-w-[7rem] flex items-center gap-2 px-4 py-2.5 text-xs font-bold whitespace-nowrap rounded-lg transition-all ${tasksTab === id ? 'bg-white text-primary-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
>
<Icon className="w-3.5 h-3.5" /> {label}
</button>
))}
</div>
<div className="p-4 min-h-[200px]">
{dashboardTasks
.filter(t => getTaskMyRole(t, currentUser.id) === tasksTab)
.length === 0 ? (
<p className="text-center py-8 text-slate-400 text-sm">Нет задач в этой роли</p>
) : (
<div className="space-y-3">
{dashboardTasks
.filter(t => getTaskMyRole(t, currentUser.id) === tasksTab)
.map(task => (
<DashboardTaskCard
key={task.id}
task={task}
myRole={getTaskMyRole(task, currentUser.id)!}
onToggleDone={() => {
const nextStatus = task.status === 'done' ? 'new' : 'done';
setDashboardTasks(prev =>
prev.map(t => (t.id === task.id ? { ...t, status: nextStatus as BuildingTask['status'], updatedAt: new Date().toISOString() } : t))
);
}}
onOpen={() => { setEditingTask(task); setIsTaskModalOpen(true); }}
/>
))}
</div>
)}
</div>
<div className="p-4 border-t border-slate-100">
<button
type="button"
onClick={() => { setEditingTask(null); setIsTaskModalOpen(true); }}
className="w-full py-3 border-2 border-dashed border-slate-200 rounded-xl text-slate-500 hover:border-primary-300 hover:text-primary-600 hover:bg-primary-50/50 text-sm font-bold flex items-center justify-center gap-2 transition-colors"
>
<Plus className="w-4 h-4" /> Добавить задачу
</button>
</div>
</div>
</div>
)}
{/* Модалка задачи (без фото отчёта) */}
{isTaskModalOpen && (
<DashboardTaskModal
isOpen={isTaskModalOpen}
onClose={() => { setIsTaskModalOpen(false); setEditingTask(null); }}
currentUser={currentUser}
task={editingTask}
onSave={(task) => {
setDashboardTasks(prev => {
const idx = prev.findIndex(t => t.id === task.id);
if (idx >= 0) return prev.map((t, i) => (i === idx ? task : t));
return [...prev, task];
});
setIsTaskModalOpen(false);
setEditingTask(null);
}}
/>
)}
{/* Модалка продвинутой статистики по отделу */}
{statsModalDepartment && (
<DepartmentStatsModal
title={
statsModalDepartment === 'production' ? 'Производство' :
statsModalDepartment === 'pr' ? 'PR и Клиентский Опыт' :
statsModalDepartment === 'finance' ? 'Финансы' :
statsModalDepartment === 'development' ? 'Отдел Развития' :
statsModalDepartment === 'legal' ? 'Юридический отдел' :
statsModalDepartment === 'hr' ? 'Отдел Кадров (HR)' :
statsModalDepartment === 'news' ? 'Офис' :
'Новости компании'
}
blockId={statsModalDepartment}
onClose={() => setStatsModalDepartment(null)}
onGoToModule={onNavigateToModule ?? undefined}
>
{statsModalDepartment === 'production' && (
<div className="space-y-8">
{productionDashboardLoading ? (
<p className="text-slate-400 text-sm">Загрузка дашборда...</p>
) : !productionDashboard ? (
<p className="text-slate-400 text-sm">Нет данных</p>
) : (
<>
{/* 4.1 Состояние домов, SLA, график работ */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Wrench className="w-4 h-4" /> Состояние и график</h4>
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-slate-50 rounded-xl">
<p className="text-xs font-bold text-slate-500 uppercase mb-1">Состояние домов</p>
<p className="text-xl font-black text-slate-800">{productionDashboard.buildingCondition.label}</p>
<p className="text-xs text-slate-500 mt-1">Средний износ {productionDashboard.buildingCondition.value > 0 ? `${productionDashboard.buildingCondition.value.toFixed(1)}%` : 'Н/Д'} · {productionDashboard.buildingCondition.buildingsWithWear} из {productionDashboard.buildingCondition.totalBuildings} домов</p>
</div>
<div className="p-4 bg-slate-50 rounded-xl">
<p className="text-xs font-bold text-slate-500 uppercase mb-1">Рейтинг SLA</p>
<p className="text-2xl font-black text-slate-800">{productionDashboard.slaRating}%</p>
</div>
</div>
<div className="mt-3 pt-3 border-t border-slate-200">
<p className="text-sm font-bold text-slate-700 mb-1">График работ</p>
<p className="text-sm text-slate-600">В срок: <span className="font-bold text-emerald-600">{productionDashboard.workSchedule.onTimeRate}%</span> · С опозданием: <span className="font-bold text-red-600">{productionDashboard.workSchedule.lateRate}%</span> · Просрочено: <span className="font-bold text-amber-600">{productionDashboard.workSchedule.overdue}</span></p>
</div>
</section>
{/* 4.2 По участкам */}
{productionDashboard.districts && productionDashboard.districts.length > 0 && (
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><LayoutGrid className="w-4 h-4" /> По участкам</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-2 font-bold text-slate-600">Участок</th>
<th className="text-left py-2 font-bold text-slate-600">Руководитель</th>
<th className="text-right py-2 font-bold text-slate-600">Домов</th>
<th className="text-right py-2 font-bold text-slate-600">Заявок</th>
<th className="text-right py-2 font-bold text-slate-600">Выполнено</th>
<th className="text-right py-2 font-bold text-slate-600">Просрочено</th>
<th className="text-right py-2 font-bold text-slate-600">Успеваемость %</th>
</tr>
</thead>
<tbody>
{productionDashboard.districts.map((d) => (
<tr key={d.districtId} className="border-b border-slate-100">
<td className="py-2 font-medium text-slate-800">{d.districtName}</td>
<td className="py-2 text-slate-600">{d.managerName}</td>
<td className="py-2 text-right">{d.buildingCount}</td>
<td className="py-2 text-right">{d.totalApplications}</td>
<td className="py-2 text-right text-emerald-600">{d.totalCompleted}</td>
<td className="py-2 text-right text-amber-600">{d.totalOverdue}</td>
<td className="py-2 text-right font-bold">{Math.round(d.completionRate)}%</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)}
{/* 4.3 Заявки — временные срезы */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Clock className="w-4 h-4" /> Заявки временные срезы</h4>
<div className="grid grid-cols-3 gap-4">
<div className="p-4 bg-amber-50 rounded-xl border border-amber-100">
<p className="text-xs font-bold text-slate-500 uppercase mb-1">Сегодня</p>
<p className="text-lg font-black text-slate-800">Создано: {productionDashboard.applicationsTimeSlices.today.created}</p>
<p className="text-sm text-slate-600">Закрыто: {productionDashboard.applicationsTimeSlices.today.closed} · Просрочено: {productionDashboard.applicationsTimeSlices.today.overdueNow}</p>
</div>
<div className="p-4 bg-blue-50 rounded-xl border border-blue-100">
<p className="text-xs font-bold text-slate-500 uppercase mb-1">Неделя</p>
<p className="text-lg font-black text-slate-800">Создано: {productionDashboard.applicationsTimeSlices.week.created}</p>
<p className="text-sm text-slate-600">Закрыто: {productionDashboard.applicationsTimeSlices.week.closed} · Просрочено: {productionDashboard.applicationsTimeSlices.week.overdueNow}</p>
</div>
<div className="p-4 bg-slate-50 rounded-xl border border-slate-200">
<p className="text-xs font-bold text-slate-500 uppercase mb-1">Месяц</p>
<p className="text-lg font-black text-slate-800">Создано: {productionDashboard.applicationsTimeSlices.month.created}</p>
<p className="text-sm text-slate-600">Закрыто: {productionDashboard.applicationsTimeSlices.month.closed} · Просрочено: {productionDashboard.applicationsTimeSlices.month.overdueNow}</p>
</div>
</div>
</section>
{/* 4.4 Заявки — общая сводка */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Inbox className="w-4 h-4" /> Заявки сводка</h4>
<div className="flex flex-wrap gap-3">
<span className="px-3 py-1.5 bg-red-50 text-red-700 rounded-lg text-xs font-bold">Новые: {productionDashboard.applicationsSummary.new}</span>
<span className="px-3 py-1.5 bg-blue-50 text-blue-700 rounded-lg text-xs font-bold">В работе: {productionDashboard.applicationsSummary.inProgress}</span>
<span className="px-3 py-1.5 bg-orange-50 text-orange-700 rounded-lg text-xs font-bold">Отложено: {productionDashboard.applicationsSummary.deferred}</span>
<span className="px-3 py-1.5 bg-emerald-50 text-emerald-700 rounded-lg text-xs font-bold">Выполнено: {productionDashboard.applicationsSummary.done}</span>
<span className="px-3 py-1.5 bg-amber-50 text-amber-700 rounded-lg text-xs font-bold flex items-center gap-1"><AlertTriangle className="w-3 h-3" /> Просрочено: {productionDashboard.applicationsSummary.overdue}</span>
<span className="px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-xs font-bold">Индекс SLA: {productionDashboard.applicationsSummary.completionRate}%</span>
</div>
</section>
{/* 4.5 Загрузка мастеров */}
{productionDashboard.topPerformers && productionDashboard.topPerformers.length > 0 && (
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><UsersRound className="w-4 h-4" /> Загрузка мастеров</h4>
<ul className="space-y-2">
{productionDashboard.topPerformers.map((p, i) => (
<li key={i} className="flex justify-between items-center py-1.5 px-3 rounded-lg bg-slate-50">
<span className="font-medium text-slate-800">{p.name}</span>
<span className={`font-bold ${p.activeCount > 5 ? 'text-amber-600' : 'text-slate-700'}`}>{p.activeCount} активных</span>
</li>
))}
</ul>
</section>
)}
{/* 4.6 План работ */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><CalendarDays className="w-4 h-4" /> План работ (текущий месяц)</h4>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Всего работ</p>
<p className="text-xl font-black text-slate-800">{productionDashboard.planWorks.total}</p>
</div>
<div className="p-3 bg-emerald-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Выполнено</p>
<p className="text-xl font-black text-emerald-700">{productionDashboard.planWorks.completed}</p>
</div>
<div className="p-3 bg-amber-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Перенесено</p>
<p className="text-xl font-black text-amber-700">{productionDashboard.planWorks.carriedOver}</p>
</div>
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Смета (мес)</p>
<p className="text-lg font-black text-slate-800">{productionDashboard.planWorks.estimatedCostMonth > 0 ? `${(productionDashboard.planWorks.estimatedCostMonth / 1000).toFixed(0)}k ₽` : '—'}</p>
</div>
</div>
{productionDashboard.planWorks.byDistrict && productionDashboard.planWorks.byDistrict.length > 0 && (
<p className="text-xs text-slate-500 mt-2">По участкам: {productionDashboard.planWorks.byDistrict.map((d) => `${d.districtId}: ${d.total} работ, выполнено ${d.completed}`).join('; ')}</p>
)}
</section>
{/* 4.7 Обходы (осмотры) */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Camera className="w-4 h-4" /> Обходы (осмотры)</h4>
<div className="flex flex-wrap gap-4">
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">За месяц</p>
<p className="text-xl font-black text-slate-800">{productionDashboard.inspections.countMonth}</p>
</div>
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">За квартал</p>
<p className="text-xl font-black text-slate-800">{productionDashboard.inspections.countQuarter}</p>
</div>
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Последний осмотр</p>
<p className="text-sm font-bold text-slate-800">{productionDashboard.inspections.lastDate ? new Date(productionDashboard.inspections.lastDate).toLocaleDateString('ru-RU') : '—'}</p>
</div>
</div>
</section>
{/* 4.8 Списания */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Package className="w-4 h-4" /> Списания</h4>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Сегодня</p>
<p className="text-xl font-black text-slate-800">{productionDashboard.writeOffs.countToday}</p>
</div>
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">За неделю</p>
<p className="text-xl font-black text-slate-800">{productionDashboard.writeOffs.countWeek}</p>
</div>
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">За месяц</p>
<p className="text-xl font-black text-slate-800">{productionDashboard.writeOffs.countMonth}</p>
</div>
<div className="p-3 bg-emerald-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Сумма (мес)</p>
<p className="text-lg font-black text-emerald-700">{productionDashboard.writeOffs.sumMonth > 0 ? `${(productionDashboard.writeOffs.sumMonth / 1000).toFixed(0)}k ₽` : '—'}</p>
</div>
</div>
{productionDashboard.districtInventorySummary && productionDashboard.districtInventorySummary.length > 0 && (
<p className="text-xs text-slate-500 mt-2">Остатки складов участков: {productionDashboard.districtInventorySummary.map((d) => `${d.districtName}: ${d.totalQuantity} ед.`).join('; ')}</p>
)}
</section>
{/* 4.9 Обходы счетчиков */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Gauge className="w-4 h-4" /> Обходы счетчиков</h4>
<div className="flex flex-wrap gap-4">
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">За месяц</p>
<p className="text-xl font-black text-slate-800">{productionDashboard.meterRounds.countMonth}</p>
</div>
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Последний обход</p>
<p className="text-sm font-bold text-slate-800">{productionDashboard.meterRounds.lastDate ? new Date(productionDashboard.meterRounds.lastDate).toLocaleDateString('ru-RU') : '—'}</p>
</div>
</div>
</section>
</>
)}
</div>
)}
{statsModalDepartment === 'pr' && (
<div className="space-y-8">
{prDashboardLoading ? (
<p className="text-slate-400 text-sm">Загрузка дашборда...</p>
) : !prDashboard ? (
<p className="text-slate-400 text-sm">Нет данных</p>
) : (
<>
{/* 1. NPS по компании */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Megaphone className="w-4 h-4" /> NPS по компании</h4>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
<div className="p-4 bg-indigo-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Индекс NPS</p>
<p className="text-2xl font-black text-indigo-700">{prDashboard.npsCompany}</p>
</div>
<div className="p-4 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Ответов за месяц</p>
<p className="text-xl font-black text-slate-800">{prDashboard.npsResponsesCount}</p>
</div>
<div className="p-4 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Средний балл</p>
<p className="text-xl font-black text-slate-800">{prDashboard.npsAvgScore > 0 ? prDashboard.npsAvgScore.toFixed(1) : '—'}</p>
</div>
</div>
<div className="flex flex-wrap gap-2 mt-2">
<span className="px-3 py-1.5 bg-emerald-50 text-emerald-700 rounded-lg text-xs font-bold">Промоутеры: {prDashboard.npsPromoters}</span>
<span className="px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-xs font-bold">Нейтралы: {prDashboard.npsPassives}</span>
<span className="px-3 py-1.5 bg-red-50 text-red-700 rounded-lg text-xs font-bold">Критики: {prDashboard.npsDetractors}</span>
</div>
</section>
{/* 2. Удовлетворённость (CSAT) */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Gauge className="w-4 h-4" /> Удовлетворённость (CSAT)</h4>
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">CSAT, %</p>
<p className="text-2xl font-black text-slate-800">{prDashboard.csatPercent}%</p>
</div>
<div className="p-4 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Средний рейтинг отзывов</p>
<p className="text-xl font-black text-slate-800">{prDashboard.reviewsAvgRating > 0 ? prDashboard.reviewsAvgRating.toFixed(1) : '—'}</p>
</div>
<div className="col-span-2 p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Всего отзывов</p>
<p className="text-lg font-black text-slate-800">{prDashboard.reviewsTotal}</p>
</div>
</div>
</section>
{/* 3. SMM — подписчики */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Share2 className="w-4 h-4" /> SMM подписчики</h4>
<div className="flex flex-wrap gap-4">
<div className="p-4 bg-indigo-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Всего</p>
<p className="text-2xl font-black text-indigo-700">{prDashboard.smm.total.toLocaleString('ru-RU')}</p>
</div>
{prDashboard.smm.byChannel && prDashboard.smm.byChannel.length > 0 && (
<div className="flex flex-wrap gap-2">
{prDashboard.smm.byChannel.map((c) => (
<span key={c.id} className="px-3 py-1.5 bg-slate-100 rounded-lg text-xs font-bold text-slate-700">
{c.type === 'tg' ? 'TG' : c.type === 'vk' ? 'VK' : c.type === 'wa' ? 'WA' : c.name}: {c.subscribersCount.toLocaleString('ru-RU')}
</span>
))}
</div>
)}
</div>
{(!prDashboard.smm.byChannel || prDashboard.smm.byChannel.length === 0) && prDashboard.smm.total === 0 && (
<p className="text-xs text-slate-400 mt-2">Добавьте каналы в модуле PR SMM</p>
)}
</section>
{/* 4. Отзывы */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><MessageSquare className="w-4 h-4" /> Отзывы</h4>
<div className="flex flex-wrap gap-2">
<span className="px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-xs font-bold">Всего: {prDashboard.reviewsStats.total}</span>
<span className="px-3 py-1.5 bg-blue-50 text-blue-700 rounded-lg text-xs font-bold">Новые: {prDashboard.reviewsStats.new_count}</span>
<span className="px-3 py-1.5 bg-emerald-50 text-emerald-700 rounded-lg text-xs font-bold">Обработано: {prDashboard.reviewsStats.processed_count}</span>
<span className="px-3 py-1.5 bg-emerald-50 text-emerald-700 rounded-lg text-xs font-bold">Положительные: {prDashboard.reviewsStats.positive_count}</span>
<span className="px-3 py-1.5 bg-red-50 text-red-700 rounded-lg text-xs font-bold">Негативные: {prDashboard.reviewsStats.negative_count}</span>
<span className="px-3 py-1.5 bg-amber-50 text-amber-700 rounded-lg text-xs font-bold">Яндекс: {prDashboard.reviewsStats.yandex_count}</span>
<span className="px-3 py-1.5 bg-amber-50 text-amber-700 rounded-lg text-xs font-bold">2GIS: {prDashboard.reviewsStats.gis2_count}</span>
</div>
</section>
{/* 5. Инциденты */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><AlertTriangle className="w-4 h-4" /> Инциденты</h4>
<div className="flex flex-wrap gap-2">
<span className="px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-xs font-bold">Всего: {prDashboard.incidentsSummary.total}</span>
<span className="px-3 py-1.5 bg-red-50 text-red-700 rounded-lg text-xs font-bold">Открытые: {prDashboard.incidentsSummary.open}</span>
<span className="px-3 py-1.5 bg-amber-50 text-amber-700 rounded-lg text-xs font-bold">В работе: {prDashboard.incidentsSummary.in_progress}</span>
<span className="px-3 py-1.5 bg-emerald-50 text-emerald-700 rounded-lg text-xs font-bold">Закрыты: {prDashboard.incidentsSummary.resolved}</span>
</div>
</section>
{/* 6. Мероприятия */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><PartyPopper className="w-4 h-4" /> Мероприятия</h4>
<div className="grid grid-cols-3 gap-4">
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">За месяц</p>
<p className="text-xl font-black text-slate-800">{prDashboard.events.countMonth}</p>
</div>
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Предстоящие</p>
<p className="text-xl font-black text-slate-800">{prDashboard.events.upcoming}</p>
</div>
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Прошедшие</p>
<p className="text-xl font-black text-slate-800">{prDashboard.events.past}</p>
</div>
</div>
</section>
{/* 7. Отчёты жителям */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><FileText className="w-4 h-4" /> Отчёты жителям</h4>
<div className="flex flex-wrap gap-4">
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Всего отчётов</p>
<p className="text-xl font-black text-slate-800">{prDashboard.residentReports.total}</p>
</div>
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Опубликовано за месяц</p>
<p className="text-xl font-black text-slate-800">{prDashboard.residentReports.publishedThisMonth}</p>
</div>
</div>
</section>
{/* 8. Фото отчёты */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Camera className="w-4 h-4" /> Фото отчёты</h4>
<div className="flex flex-wrap gap-4">
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Всего</p>
<p className="text-xl font-black text-slate-800">{prDashboard.workPhotos.total}</p>
</div>
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">За месяц</p>
<p className="text-xl font-black text-slate-800">{prDashboard.workPhotos.thisMonth}</p>
</div>
</div>
</section>
{/* 9. NPS опросы */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><ClipboardList className="w-4 h-4" /> NPS опросы</h4>
<div className="flex flex-wrap gap-2 mb-2">
<span className="px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-xs font-bold">Всего: {prDashboard.npsSurveys.total}</span>
<span className="px-3 py-1.5 bg-indigo-50 text-indigo-700 rounded-lg text-xs font-bold">Активных: {prDashboard.npsSurveys.active}</span>
</div>
{prDashboard.npsSurveys.list && prDashboard.npsSurveys.list.length > 0 && (
<ul className="space-y-1.5 text-sm text-slate-600">
{prDashboard.npsSurveys.list.map((s) => (
<li key={s.id} className="flex justify-between items-center py-1 border-b border-slate-100 last:border-0">
<span className="font-medium truncate mr-2">{s.title}</span>
<span className="text-xs font-bold text-slate-500">{s.responsesCount} ответов · {s.status}</span>
</li>
))}
</ul>
)}
</section>
{/* 10. Отложенные посты */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Send className="w-4 h-4" /> Отложенные посты</h4>
<div className="flex flex-wrap gap-2">
<span className="px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-xs font-bold">Черновик: {prDashboard.scheduledPosts.draft}</span>
<span className="px-3 py-1.5 bg-amber-50 text-amber-700 rounded-lg text-xs font-bold">На модерации: {prDashboard.scheduledPosts.pending_approval}</span>
<span className="px-3 py-1.5 bg-blue-50 text-blue-700 rounded-lg text-xs font-bold">Одобрено: {prDashboard.scheduledPosts.approved}</span>
<span className="px-3 py-1.5 bg-emerald-50 text-emerald-700 rounded-lg text-xs font-bold">Опубликовано: {prDashboard.scheduledPosts.published}</span>
<span className="px-3 py-1.5 bg-red-50 text-red-700 rounded-lg text-xs font-bold">Отклонено: {prDashboard.scheduledPosts.rejected}</span>
<span className="px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-xs font-bold">На редактировании: {prDashboard.scheduledPosts.edited}</span>
</div>
</section>
{/* 11. Негатив в работе */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><AlertTriangle className="w-4 h-4" /> Негатив в работе</h4>
<div className="flex flex-wrap gap-4">
<div className="p-3 bg-red-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Открытые инциденты</p>
<p className="text-xl font-black text-red-700">{prDashboard.negative.openIncidents}</p>
</div>
<div className="p-3 bg-amber-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Негативных отзывов</p>
<p className="text-xl font-black text-amber-700">{prDashboard.negative.negativeReviewsCount}</p>
</div>
</div>
</section>
</>
)}
</div>
)}
{statsModalDepartment === 'finance' && (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-emerald-50 rounded-xl">
<p className="text-xs font-bold text-slate-500 uppercase mb-1">Чистая прибыль (мес)</p>
<p className="text-2xl font-black text-emerald-700">1.2M </p>
<span className="text-xs text-emerald-600 font-bold flex items-center gap-1 mt-1"><TrendingUp className="w-3 h-3" /> тренд вверх</span>
</div>
<div className="p-4 bg-slate-50 rounded-xl">
<p className="text-xs font-bold text-slate-500 uppercase mb-1">Общая дебиторка</p>
<p className="text-2xl font-black text-slate-800">8.5M </p>
</div>
</div>
<p className="text-sm text-slate-600">В модуле «Финансы» доступны кассовый план, реестр счетов, платёжный календарь и отчёты по домам.</p>
</div>
)}
{statsModalDepartment === 'development' && (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-sky-50 rounded-xl">
<p className="text-xs font-bold text-slate-500 uppercase mb-1">Новых дома (квартал)</p>
<p className="text-2xl font-black text-sky-700">+3</p>
</div>
<div className="p-4 bg-slate-50 rounded-xl">
<p className="text-xs font-bold text-slate-500 uppercase mb-1">Домов в воронке</p>
<p className="text-2xl font-black text-slate-800">12</p>
</div>
<div className="col-span-2 p-4 bg-slate-50 rounded-xl">
<p className="text-xs font-bold text-slate-500 uppercase mb-2">Конверсия</p>
<div className="w-full bg-slate-200 rounded-full h-3"><div className="bg-sky-500 h-3 rounded-full" style={{ width: '25%' }} /></div>
<p className="text-sm font-bold text-slate-700 mt-1">25%</p>
</div>
</div>
<p className="text-sm text-slate-600">В модуле «Развитие» воронка продаж, ОСС, маркетинг и технический аудит.</p>
</div>
)}
{statsModalDepartment === 'legal' && (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-slate-100 rounded-xl">
<p className="text-xs font-bold text-slate-500 uppercase mb-1">Взыскано за квартал</p>
<p className="text-2xl font-black text-slate-800">2.1M </p>
<span className="text-xs text-emerald-600 font-bold flex items-center gap-1 mt-1"><TrendingUp className="w-3 h-3" /> тренд вверх</span>
</div>
<div className="p-4 bg-slate-50 rounded-xl">
<p className="text-xs font-bold text-slate-500 uppercase mb-1">Активных судебных дел</p>
<p className="text-2xl font-black text-slate-800">42</p>
</div>
<div className="col-span-2 p-4 bg-slate-50 rounded-xl">
<p className="text-xs font-bold text-slate-500 uppercase mb-2">Процент выигранных дел</p>
<div className="w-full bg-slate-200 rounded-full h-3"><div className="bg-emerald-500 h-3 rounded-full" style={{ width: '85%' }} /></div>
<p className="text-sm font-bold text-slate-700 mt-1">85%</p>
</div>
</div>
<p className="text-sm text-slate-600">В модуле «Юр. отдел» досудебная работа, договоры, судебные дела и проверки контрагентов.</p>
</div>
)}
{statsModalDepartment === 'hr' && (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-violet-50 rounded-xl">
<p className="text-xs font-bold text-slate-500 uppercase mb-1">Всего сотрудников</p>
<p className="text-2xl font-black text-violet-700">24</p>
</div>
<div className="p-4 bg-slate-50 rounded-xl">
<p className="text-xs font-bold text-slate-500 uppercase mb-1">Кандидатов в воронке</p>
<p className="text-2xl font-black text-slate-800">2</p>
</div>
<div className="col-span-2 p-4 bg-slate-50 rounded-xl">
<p className="text-xs font-bold text-slate-500 uppercase mb-2">Текучесть кадров (год)</p>
<div className="w-full bg-slate-200 rounded-full h-3"><div className="bg-violet-500 h-3 rounded-full" style={{ width: '12%' }} /></div>
<p className="text-sm font-bold text-slate-700 mt-1">12%</p>
</div>
</div>
<p className="text-sm text-slate-600">В модуле «Кадры» штат, вакансии, кандидаты, отпуска и календарь событий.</p>
</div>
)}
{statsModalDepartment === 'news' && (
<div className="space-y-8">
{officeDashboardLoading ? (
<p className="text-slate-400 text-sm">Загрузка дашборда...</p>
) : !officeDashboard ? (
<p className="text-slate-400 text-sm">Нет данных</p>
) : (
<>
{/* 1. Новости компании */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Newspaper className="w-4 h-4" /> Новости компании</h4>
<div className="flex flex-wrap gap-2">
<span className="px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-xs font-bold">Всего: {officeDashboard.companyNews.total}</span>
<span className="px-3 py-1.5 bg-emerald-50 text-emerald-700 rounded-lg text-xs font-bold">Опубликовано: {officeDashboard.companyNews.published}</span>
<span className="px-3 py-1.5 bg-amber-50 text-amber-700 rounded-lg text-xs font-bold">Черновики: {officeDashboard.companyNews.draft}</span>
<span className="px-3 py-1.5 bg-blue-50 text-blue-700 rounded-lg text-xs font-bold">На модерации: {officeDashboard.companyNews.pending}</span>
<span className="px-3 py-1.5 bg-indigo-50 text-indigo-700 rounded-lg text-xs font-bold">За месяц: {officeDashboard.companyNews.publishedThisMonth}</span>
</div>
</section>
{/* 2. Оборудование */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Package className="w-4 h-4" /> Оборудование</h4>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Всего</p>
<p className="text-xl font-black text-slate-800">{officeDashboard.equipment.total}</p>
</div>
<div className="p-3 bg-emerald-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Хорошее</p>
<p className="text-xl font-black text-emerald-700">{officeDashboard.equipment.byCondition.good}</p>
</div>
<div className="p-3 bg-amber-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Удовлетв.</p>
<p className="text-xl font-black text-amber-700">{officeDashboard.equipment.byCondition.fair}</p>
</div>
<div className="p-3 bg-red-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Плохое</p>
<p className="text-xl font-black text-red-700">{officeDashboard.equipment.byCondition.poor}</p>
</div>
<div className="col-span-2 sm:col-span-4 p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Гарантия истекает (30 дн.)</p>
<p className="text-lg font-black text-slate-800">{officeDashboard.equipment.warrantyExpiringSoon}</p>
</div>
</div>
</section>
{/* 3. Заявки на ремонт */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Wrench className="w-4 h-4" /> Заявки на ремонт</h4>
<div className="flex flex-wrap gap-2">
<span className="px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-xs font-bold">Всего: {officeDashboard.repairRequests.total}</span>
<span className="px-3 py-1.5 bg-red-50 text-red-700 rounded-lg text-xs font-bold">Открытые: {officeDashboard.repairRequests.open}</span>
<span className="px-3 py-1.5 bg-amber-50 text-amber-700 rounded-lg text-xs font-bold">В работе: {officeDashboard.repairRequests.in_progress}</span>
<span className="px-3 py-1.5 bg-emerald-50 text-emerald-700 rounded-lg text-xs font-bold">Выполнено: {officeDashboard.repairRequests.completed}</span>
</div>
</section>
{/* 4. Заявки на закупку */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Package className="w-4 h-4" /> Заявки на закупку</h4>
<div className="flex flex-wrap gap-2">
<span className="px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-xs font-bold">Всего: {officeDashboard.supplyRequests.total}</span>
{Object.entries(officeDashboard.supplyRequests.byStatus || {}).map(([status, cnt]) => (
<span key={status} className="px-3 py-1.5 bg-slate-50 text-slate-700 rounded-lg text-xs font-bold">{status}: {cnt}</span>
))}
</div>
</section>
{/* 5. Инвентарь */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Package className="w-4 h-4" /> Инвентарь</h4>
<div className="flex flex-wrap gap-4">
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Позиций всего</p>
<p className="text-xl font-black text-slate-800">{officeDashboard.inventory.totalItems}</p>
</div>
<div className="p-3 bg-amber-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Низкий остаток</p>
<p className="text-xl font-black text-amber-700">{officeDashboard.inventory.lowStockCount}</p>
</div>
</div>
</section>
{/* 6. Документооборот */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><FileText className="w-4 h-4" /> Документооборот</h4>
<div className="flex flex-wrap gap-2 mb-2">
<span className="px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-xs font-bold">Всего: {officeDashboard.documents.total}</span>
<span className="px-3 py-1.5 bg-blue-50 text-blue-700 rounded-lg text-xs font-bold">Входящие: {officeDashboard.documents.incoming}</span>
<span className="px-3 py-1.5 bg-indigo-50 text-indigo-700 rounded-lg text-xs font-bold">Исходящие: {officeDashboard.documents.outgoing}</span>
</div>
{Object.keys(officeDashboard.documents.byStatus || {}).length > 0 && (
<div className="flex flex-wrap gap-2">
{Object.entries(officeDashboard.documents.byStatus).map(([status, cnt]) => (
<span key={status} className="px-2 py-1 bg-slate-50 text-slate-600 rounded text-xs font-bold">{status}: {cnt}</span>
))}
</div>
)}
</section>
{/* 7. Заказы */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Package className="w-4 h-4" /> Заказы</h4>
<div className="flex flex-wrap gap-2">
<span className="px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-xs font-bold">Всего: {officeDashboard.orders.total}</span>
{Object.entries(officeDashboard.orders.byStatus || {}).map(([status, cnt]) => (
<span key={status} className="px-3 py-1.5 bg-slate-50 text-slate-700 rounded-lg text-xs font-bold">{status}: {cnt}</span>
))}
</div>
</section>
{/* 8. Совещания */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><Calendar className="w-4 h-4" /> Совещания</h4>
<div className="flex flex-wrap gap-4">
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">На эту неделю</p>
<p className="text-xl font-black text-slate-800">{officeDashboard.meetings.thisWeek}</p>
</div>
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Предстоящие</p>
<p className="text-xl font-black text-slate-800">{officeDashboard.meetings.upcoming}</p>
</div>
</div>
</section>
{/* 9. Переговорные */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><LayoutGrid className="w-4 h-4" /> Переговорные</h4>
<div className="p-3 bg-slate-50 rounded-xl inline-block">
<p className="text-xs text-slate-500 uppercase font-bold">Комнат</p>
<p className="text-xl font-black text-slate-800">{officeDashboard.meetingRooms.total}</p>
</div>
</section>
{/* 10. База знаний */}
<section>
<h4 className="text-sm font-bold text-slate-500 uppercase mb-3 flex items-center gap-2"><ClipboardList className="w-4 h-4" /> База знаний</h4>
<div className="flex flex-wrap gap-4">
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Категорий</p>
<p className="text-xl font-black text-slate-800">{officeDashboard.knowledgeBase.categoriesCount}</p>
</div>
<div className="p-3 bg-slate-50 rounded-xl">
<p className="text-xs text-slate-500 uppercase font-bold">Статей</p>
<p className="text-xl font-black text-slate-800">{officeDashboard.knowledgeBase.articlesCount}</p>
</div>
</div>
</section>
</>
)}
</div>
)}
</DepartmentStatsModal>
)}
{/* Strategic Goals Modal */}
{isGoalsModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-sm animate-fade-in" onClick={() => setIsGoalsModalOpen(false)}>
<div className="bg-slate-50 rounded-[2rem] w-full max-w-5xl max-h-[90vh] overflow-y-auto shadow-2xl animate-slide-up" onClick={e => e.stopPropagation()}>
{/* Modal Header */}
<div className="p-8 border-b border-slate-200 flex justify-between items-start sticky top-0 bg-slate-50 z-10 rounded-t-[2rem]">
<div>
<div className="flex items-center gap-2 text-primary-600 mb-1">
<Target className="w-6 h-6" />
<span className="text-xs font-black uppercase tracking-[0.2em]">Стратегическое планирование</span>
</div>
<h3 className="text-3xl font-black text-slate-900">Цели компании 2024</h3>
</div>
<button
onClick={() => setIsGoalsModalOpen(false)}
className="p-3 bg-white hover:bg-red-50 text-slate-400 hover:text-red-500 rounded-2xl transition-all shadow-sm border border-slate-200"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Modal Content */}
<div className="p-8">
{filteredGoals.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredGoals.map(goal => <GoalCard key={goal.id} goal={goal} />)}
</div>
) : (
<div className="py-20 text-center text-slate-400 italic">
Нет доступных целей для вашей роли
</div>
)}
</div>
{/* Modal Footer */}
<div className="p-8 bg-white border-t border-slate-200 rounded-b-[2rem] flex justify-between items-center">
<p className="text-sm text-slate-500 font-medium italic">Обновлено: {new Date().toLocaleDateString('ru-RU')}</p>
{currentUserRole === 'DIRECTOR' && (
<button className="flex items-center gap-2 px-6 py-3 bg-slate-900 text-white rounded-xl font-bold text-sm hover:bg-slate-800 transition-all shadow-lg active:scale-95">
<Pencil className="w-4 h-4" /> Редактировать показатели
</button>
)}
</div>
</div>
</div>
)}
{/* Модалка «Предложить новость» */}
{isSuggestNewsModalOpen && (
<SuggestNewsModal
onClose={() => setIsSuggestNewsModalOpen(false)}
onSuccess={() => {
setIsSuggestNewsModalOpen(false);
backendApi.getCompanyNews({ status: 'published', limit: 5 }).then((list) => Array.isArray(list) && setCompanyNews(list));
}}
/>
)}
{/* Модалка просмотра новости (из уведомления) */}
{openNewsId != null && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-sm animate-fade-in" onClick={() => onCloseNews?.()}>
<div className="bg-white rounded-2xl w-full max-w-lg max-h-[85vh] overflow-y-auto shadow-2xl animate-slide-up" onClick={(e) => e.stopPropagation()}>
<div className="p-6 border-b border-slate-200 flex justify-between items-start sticky top-0 bg-white z-10 rounded-t-2xl">
<h3 className="text-lg font-black text-slate-900">Новость компании</h3>
<button type="button" onClick={() => onCloseNews?.()} className="p-2 text-slate-400 hover:text-slate-600 rounded-xl hover:bg-slate-100">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6">
{viewNewsLoading ? (
<div className="text-center py-8 text-slate-400 text-sm">Загрузка...</div>
) : viewNewsItem ? (
<>
<p className="font-bold text-slate-800 text-xl">{viewNewsItem.title}</p>
<p className="text-xs text-slate-500 mt-1">
{viewNewsItem.publishedAt
? new Date(viewNewsItem.publishedAt).toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' })
: new Date(viewNewsItem.createdAt).toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' })}
{viewNewsItem.createdByName && ` · ${viewNewsItem.createdByName}`}
</p>
{viewNewsItem.body && <div className="mt-4 text-slate-700 whitespace-pre-wrap">{viewNewsItem.body}</div>}
</>
) : (
<p className="text-slate-500 text-sm">Новость не найдена</p>
)}
</div>
</div>
</div>
)}
</div>
);
};
// ====================================================================================
// Suggest News Modal — предложить новость (черновик), выбор уведомлений по отделам и лично
// ====================================================================================
const DEPT_LABELS: Record<Department, string> = {
production: 'Производство',
pr: 'PR',
finance: 'Финансы',
development: 'Развитие',
legal: 'Юр. отдел',
hr: 'Кадры',
};
const SuggestNewsModal: React.FC<{ onClose: () => void; onSuccess: () => void }> = ({ onClose, onSuccess }) => {
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [notifyDepartments, setNotifyDepartments] = useState<Department[]>([]);
const [notifyEmployeeIds, setNotifyEmployeeIds] = useState<string[]>([]);
const [employeesList, setEmployeesList] = useState<Array<{ id: string; name: string }>>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
backendApi.getEmployeesList().then((list) => setEmployeesList(Array.isArray(list) ? list : [])).catch(() => setEmployeesList([]));
}, []);
const toggleDepartment = (d: Department) => {
setNotifyDepartments((prev) => (prev.includes(d) ? prev.filter((x) => x !== d) : [...prev, d]));
};
const toggleEmployee = (id: string) => {
setNotifyEmployeeIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) {
setError('Укажите заголовок');
return;
}
setError(null);
setLoading(true);
try {
await backendApi.createCompanyNews({
title: title.trim(),
body: body.trim() || null,
status: 'draft',
notifyDepartments: notifyDepartments.length ? notifyDepartments : undefined,
notifyEmployeeIds: notifyEmployeeIds.length ? notifyEmployeeIds : undefined,
});
window.dispatchEvent(new CustomEvent('mkd-news-changed'));
onSuccess();
} catch (err: any) {
setError(err?.message || 'Не удалось сохранить новость');
} finally {
setLoading(false);
}
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-sm animate-fade-in" onClick={onClose}>
<div className="bg-white rounded-2xl w-full max-w-xl max-h-[90vh] overflow-y-auto shadow-2xl animate-slide-up" onClick={(e) => e.stopPropagation()}>
<div className="p-6 border-b border-slate-200 flex justify-between items-center sticky top-0 bg-white z-10 rounded-t-2xl">
<h3 className="text-xl font-black text-slate-900">Предложить новость</h3>
<button type="button" onClick={onClose} className="p-2 text-slate-400 hover:text-slate-600 rounded-xl hover:bg-slate-100">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">Заголовок *</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-medium text-slate-800 placeholder-slate-400 focus:ring-2 focus:ring-primary-500 focus:border-transparent"
placeholder="Заголовок новости"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">Текст</label>
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
rows={4}
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm text-slate-800 placeholder-slate-400 focus:ring-2 focus:ring-primary-500 focus:border-transparent resize-y"
placeholder="Текст новости"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-2">Уведомить по отделам</label>
<div className="flex flex-wrap gap-2">
{(['production', 'pr', 'finance', 'development', 'legal', 'hr'] as Department[]).map((d) => (
<button
key={d}
type="button"
onClick={() => toggleDepartment(d)}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-colors ${notifyDepartments.includes(d) ? 'bg-primary-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'}`}
>
{DEPT_LABELS[d]}
</button>
))}
</div>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-2">Уведомить лично (сотрудники)</label>
<div className="max-h-32 overflow-y-auto border border-slate-200 rounded-xl p-2 space-y-1">
{employeesList.length === 0 && <p className="text-xs text-slate-400 py-2">Список сотрудников загружается...</p>}
{employeesList.map((emp) => (
<label key={emp.id} className="flex items-center gap-2 py-1 px-2 rounded-lg hover:bg-slate-50 cursor-pointer">
<input
type="checkbox"
checked={notifyEmployeeIds.includes(emp.id)}
onChange={() => toggleEmployee(emp.id)}
className="rounded border-slate-300 text-primary-600 focus:ring-primary-500"
/>
<span className="text-sm text-slate-700">{emp.name}</span>
</label>
))}
</div>
</div>
{error && <p className="text-sm text-red-600 font-medium">{error}</p>}
<div className="flex gap-3 pt-2">
<button type="button" onClick={onClose} className="flex-1 py-3 border border-slate-200 rounded-xl font-bold text-slate-600 hover:bg-slate-50">
Отмена
</button>
<button type="submit" disabled={loading} className="flex-1 py-3 bg-primary-600 text-white rounded-xl font-bold hover:bg-primary-700 disabled:opacity-50">
{loading ? 'Сохранение...' : 'Сохранить как черновик'}
</button>
</div>
</form>
</div>
</div>
);
};