108 lines
5.6 KiB
TypeScript
108 lines
5.6 KiB
TypeScript
|
|
|
|||
|
|
import React, { useMemo } from 'react';
|
|||
|
|
import { DomaApplication } from '../../types';
|
|||
|
|
import { LayoutGrid, Building2, Inbox, AlertTriangle, ChevronRight, Users, Trash2, Pencil } from 'lucide-react';
|
|||
|
|
|
|||
|
|
const UNASSIGNED_MANAGER = 'Не назначен';
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
title: string;
|
|||
|
|
subtitle?: string;
|
|||
|
|
applications: DomaApplication[];
|
|||
|
|
onClick: () => void;
|
|||
|
|
type: 'district' | 'building';
|
|||
|
|
buildingCount?: number;
|
|||
|
|
staffCount?: number;
|
|||
|
|
onDelete?: () => void;
|
|||
|
|
onViewStaff?: () => void;
|
|||
|
|
onEdit?: () => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const PerformanceCard: React.FC<Props> = ({ title, subtitle, applications, onClick, type, buildingCount, staffCount, onDelete, onViewStaff, onEdit }) => {
|
|||
|
|
const stats = useMemo(() => {
|
|||
|
|
const now = new Date();
|
|||
|
|
const active = applications.filter(a => a.status === 'new' || a.status === 'in_progress');
|
|||
|
|
const overdue = active.filter(a => a.deadlineAt && new Date(a.deadlineAt) < now);
|
|||
|
|
const newApps = applications.filter(a => a.status === 'new');
|
|||
|
|
const performance = active.length > 0 ? Math.round((1 - (overdue.length / active.length)) * 100) : 100;
|
|||
|
|
return { overdue, newApps, performance };
|
|||
|
|
}, [applications]);
|
|||
|
|
|
|||
|
|
const perfColor = stats.performance > 85 ? 'text-emerald-500' : stats.performance > 60 ? 'text-amber-500' : 'text-red-500';
|
|||
|
|
const Icon = type === 'district' ? LayoutGrid : Building2;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
onClick={onClick}
|
|||
|
|
className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow cursor-pointer active:scale-[0.99] flex gap-4 items-center group relative"
|
|||
|
|
style={{ position: 'relative' }}
|
|||
|
|
>
|
|||
|
|
<div className="p-3 bg-primary-50 rounded-lg text-primary-600 hidden sm:block">
|
|||
|
|
<Icon className="w-6 h-6"/>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex-1 min-w-0 pr-12">
|
|||
|
|
<h4 className="font-bold text-slate-800 truncate">{title}</h4>
|
|||
|
|
{(subtitle != null && subtitle !== '') && (
|
|||
|
|
<p className={`text-xs truncate ${subtitle === UNASSIGNED_MANAGER ? 'text-red-600 font-semibold' : 'text-slate-500'}`}>{subtitle}</p>
|
|||
|
|
)}
|
|||
|
|
<div className="flex flex-wrap gap-x-4 gap-y-1 mt-2 text-xs">
|
|||
|
|
{type === 'district' && (
|
|||
|
|
<div className="flex items-center gap-1.5 text-slate-500">
|
|||
|
|
<Building2 className="w-3.5 h-3.5" /> {buildingCount} домов
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
<div className="flex items-center gap-1.5 text-slate-500">
|
|||
|
|
<Inbox className="w-3.5 h-3.5 text-blue-500" /> <span className="font-bold text-slate-700">{stats.newApps.length}</span> новых
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center gap-1.5 text-slate-500">
|
|||
|
|
<AlertTriangle className="w-3.5 h-3.5 text-red-500" /> <span className="font-bold text-slate-700">{stats.overdue.length}</span> просрочено
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="text-right flex items-center gap-4">
|
|||
|
|
{type === 'district' && onViewStaff && (
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
{staffCount !== undefined && (
|
|||
|
|
<span className="text-sm font-bold text-slate-600 bg-slate-100 px-2 py-1 rounded-lg">
|
|||
|
|
{staffCount}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
<button onClick={(e) => { e.stopPropagation(); onViewStaff(); }} className="p-2 bg-slate-50 hover:bg-primary-50 text-slate-400 hover:text-primary-600 rounded-lg transition-colors border border-slate-100">
|
|||
|
|
<Users className="w-5 h-5"/>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
<div className="hidden md:block">
|
|||
|
|
<p className={`text-2xl font-black ${perfColor}`}>{stats.performance}%</p>
|
|||
|
|
<p className="text-[10px] text-slate-400 font-bold uppercase">Успеваемость</p>
|
|||
|
|
</div>
|
|||
|
|
<ChevronRight className="w-5 h-5 text-slate-300 group-hover:text-primary-400 transition-colors" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="absolute top-2 right-2 flex items-center gap-1 z-30">
|
|||
|
|
{type === 'district' && onEdit && (
|
|||
|
|
<button
|
|||
|
|
onClick={(e) => { e.stopPropagation(); e.preventDefault(); onEdit(); }}
|
|||
|
|
className="p-1.5 bg-white hover:bg-primary-50 text-slate-400 hover:text-primary-600 rounded-full transition-all border border-slate-200 shadow-md hover:shadow-lg"
|
|||
|
|
title="Редактировать участок"
|
|||
|
|
type="button"
|
|||
|
|
>
|
|||
|
|
<Pencil className="w-3.5 h-3.5" />
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
{onDelete && (
|
|||
|
|
<button
|
|||
|
|
onClick={(e) => { e.stopPropagation(); e.preventDefault(); onDelete(); }}
|
|||
|
|
className="p-1.5 bg-white hover:bg-red-50 text-slate-400 hover:text-red-500 rounded-full transition-all border border-slate-200 shadow-md hover:shadow-lg"
|
|||
|
|
title="Удалить"
|
|||
|
|
type="button"
|
|||
|
|
>
|
|||
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|