Files

197 lines
8.9 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
import React, { useMemo } from 'react';
import {
Inbox,
Clock,
CheckCircle2,
AlertTriangle,
TrendingUp,
Activity,
Users,
Zap
} from 'lucide-react';
import { DomaApplication } from '../../types';
interface Props {
applications: DomaApplication[];
onNavigate: (tab: any) => void;
}
export const AppSummary: React.FC<Props> = ({ applications, onNavigate }) => {
// Используем поле isOverdue из БД, если есть, иначе вычисляем
const stats = {
new: applications.filter(a => a.status === 'new').length,
inProgress: applications.filter(a => a.status === 'in_progress').length,
deferred: applications.filter(a => a.status === 'deferred').length,
done: applications.filter(a => a.status === 'done').length,
overdue: applications.filter(a =>
a.isOverdue !== undefined
? a.isOverdue
: (a.status !== 'done' && a.status !== 'canceled' && new Date(a.deadlineAt) < new Date())
).length
};
const totalActive = stats.new + stats.inProgress + stats.deferred;
const completionRate = applications.length > 0 ? Math.round((stats.done / (stats.done + totalActive)) * 100) : 0;
// Группируем заявки по исполнителям для "Загрузка мастеров"
const performerStats = useMemo(() => {
const map = new Map<string, number>();
applications.forEach(app => {
if (app.performer?.name && app.status !== 'done' && app.status !== 'canceled') {
const count = map.get(app.performer.name) || 0;
map.set(app.performer.name, count + 1);
}
});
return Array.from(map.entries())
.map(([name, count]) => ({ name, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 4); // Топ-4 исполнителя
}, [applications]);
return (
<div className="space-y-6 animate-fade-in">
{/* Real-time KPI Cards */}
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
<StatCard
icon={Inbox}
label="Новые"
value={stats.new}
color="text-red-600"
bg="bg-red-50"
onClick={() => onNavigate('registry')}
/>
<StatCard
icon={Activity}
label="В работе"
value={stats.inProgress}
color="text-blue-600"
bg="bg-blue-50"
onClick={() => onNavigate('registry')}
/>
<StatCard
icon={Clock}
label="Отложено"
value={stats.deferred}
color="text-orange-600"
bg="bg-orange-50"
onClick={() => onNavigate('registry')}
/>
<StatCard
icon={CheckCircle2}
label="Выполнено"
value={stats.done}
color="text-emerald-600"
bg="bg-emerald-50"
/>
<StatCard
icon={AlertTriangle}
label="Просрочено"
value={stats.overdue}
color="text-amber-600"
bg="bg-amber-50"
onClick={() => onNavigate('control')}
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Progress Widget */}
<div className="lg:col-span-2 bg-slate-900 rounded-[2.5rem] p-8 text-white shadow-xl relative overflow-hidden">
<Zap className="absolute -top-4 -right-4 w-40 h-40 opacity-10 rotate-12 text-primary-400" />
<div className="relative z-10">
<div className="flex justify-between items-start mb-8">
<div>
<h3 className="text-3xl font-black mb-2">Производительность</h3>
<p className="text-slate-400 text-sm font-medium">Эффективность закрытия заявок за сегодня</p>
</div>
<div className="text-right">
<p className="text-5xl font-black text-primary-400">{completionRate}%</p>
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-500 mt-1">Индекс SLA</p>
</div>
</div>
<div className="space-y-6">
<StatusLine
label="Новые заявки"
value={applications.length > 0 ? Math.round((stats.new / applications.length) * 100) : 0}
color="bg-red-500"
/>
<StatusLine
label="В работе"
value={applications.length > 0 ? Math.round((stats.inProgress / applications.length) * 100) : 0}
color="bg-blue-500"
/>
<StatusLine
label="Выполнено"
value={completionRate}
color="bg-emerald-500"
/>
</div>
</div>
</div>
{/* Performer Load */}
<div className="bg-white rounded-[2.5rem] p-6 border border-slate-200 shadow-sm">
<h4 className="font-black text-slate-800 text-[10px] uppercase tracking-widest mb-6 flex items-center gap-2">
<Users className="w-4 h-4 text-primary-500"/> Загрузка мастеров
</h4>
<div className="space-y-4">
{performerStats.length > 0 ? (
performerStats.map((perf, idx) => (
<PerformerItem
key={idx}
name={perf.name}
count={perf.count}
warning={perf.count > 5}
/>
))
) : (
<p className="text-xs text-slate-400 italic text-center py-4">Нет активных заявок</p>
)}
</div>
<button onClick={() => onNavigate('control')} className="w-full mt-6 py-3 bg-slate-50 hover:bg-slate-100 rounded-2xl text-[10px] font-black uppercase tracking-widest text-slate-500 transition-colors">
Перераспределить
</button>
</div>
</div>
</div>
);
};
const StatCard = ({ icon: Icon, label, value, color, bg, onClick }: any) => (
<div
onClick={onClick}
className="bg-white p-5 rounded-[2rem] border border-slate-200 shadow-sm cursor-pointer hover:border-primary-400 transition-all hover:shadow-md active:scale-95"
>
<div className="flex justify-between items-start mb-3">
<div className={`p-2.5 ${bg} ${color} rounded-2xl`}>
<Icon className="w-5 h-5"/>
</div>
{value > 5 && label === 'Новые' && <span className="w-2 h-2 rounded-full bg-red-500 animate-ping"/>}
</div>
<p className="text-3xl font-black text-slate-800 leading-none">{value}</p>
<p className="text-[10px] text-slate-500 font-bold uppercase tracking-wider mt-2">{label}</p>
</div>
);
const StatusLine = ({ label, value, color }: any) => (
<div className="space-y-1.5">
<div className="flex justify-between text-[10px] font-bold uppercase text-slate-400">
<span>{label}</span>
<span className="text-white">{value}%</span>
</div>
<div className="h-1.5 w-full bg-white/10 rounded-full overflow-hidden">
<div className={`h-full ${color} rounded-full transition-all duration-1000`} style={{ width: `${value}%` }} />
</div>
</div>
);
const PerformerItem = ({ name, count, warning }: any) => (
<div className="flex items-center justify-between p-3 bg-slate-50 rounded-2xl border border-slate-100">
<span className="text-xs font-bold text-slate-700 truncate mr-2">{name}</span>
<span className={`px-2 py-0.5 rounded-lg text-[10px] font-black ${warning ? 'bg-red-100 text-red-600' : 'bg-primary-100 text-primary-600'}`}>
{count} заяв.
</span>
</div>
);