Files
mkd/components/applications/AppSummary.tsx
2026-02-04 00:17:04 +05:00

197 lines
8.9 KiB
TypeScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);