← Назадimport { authFetch } from '../../lib/api';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import type { ReactNode } from 'react';
import { useState } from 'react';
import { ChevronUp, ChevronDown, GripVertical, ChevronRight, Palette, PlusCircle, GitBranch } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
interface SortableProjectCardProps {
id: string;
children: ReactNode;
onMoveUp?: () => void;
onMoveDown?: () => void;
isFirst?: boolean;
isLast?: boolean;
isCollapsed?: boolean;
onToggleCollapse?: () => void;
cardColor?: string;
onColorChange?: (color: string) => void;
onAddTask?: () => void;
}
const PRESET_COLORS = [
'#3b82f6', // blue (primary)
'#10b981', // emerald
'#f59e0b', // amber
'#ef4444', // red
'#8b5cf6', // violet
'#ec4899', // pink
'#0088cc', // telegram blue
'#ff0000', // youtube red
'#6b7280', // gray
];
async function fetchTaskStats(projectId: string) {
const res = await authFetch(`/api/projects/${projectId}/kanban-tasks`);
if (!res.ok) return null;
const data = await res.json();
return data.stats as { total: number; proposed: number; approved: number };
}
async function fetchDeployLog(projectId: string) {
const res = await authFetch(`/api/deploy-logs/${projectId}`);
if (!res.ok) return null;
const logs = await res.json();
return logs[0] ?? null; // most recent
}
export function SortableProjectCard({
id,
children,
onMoveUp,
onMoveDown,
isFirst,
isLast,
isCollapsed,
onToggleCollapse,
cardColor,
onColorChange,
onAddTask,
}: SortableProjectCardProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const [showColorPicker, setShowColorPicker] = useState(false);
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.6 : 1,
zIndex: isDragging ? 50 : 1,
};
const { data: stats } = useQuery({
queryKey: ['kanban-tasks-stats', id],
queryFn: () => fetchTaskStats(id),
refetchInterval: 60000,
staleTime: 30000,
});
const { data: deployLog } = useQuery({
queryKey: ['deploy-log', id],
queryFn: () => fetchDeployLog(id),
refetchInterval: 120000,
staleTime: 60000,
});
// Overdue count calculation — placeholder for future implementation
// const overdueCount = useMemo(() => { ... }, [tasks]);
const deployStatusColor = !deployLog
? ''
: deployLog.status === 'success'
? 'text-emerald-400'
: deployLog.status === 'failure'
? 'text-red-400'
: 'text-gray-400';
const deployRelative = deployLog
? (() => {
const diff = Math.round((Date.now() - new Date(deployLog.timestamp).getTime()) / 60000);
if (diff < 60) return `${diff}м назад`;
if (diff < 1440) return `${Math.round(diff / 60)}ч назад`;
return `${Math.round(diff / 1440)}д назад`;
})()
: null;
return (
<div ref={setNodeRef} style={style}>
{/* Control strip */}
<div className="flex items-center justify-between px-1 pb-1 gap-1">
{/* Left: drag handle + collapse */}
<div className="flex items-center gap-0.5">
<div
{...attributes}
{...listeners}
className="flex items-center gap-1 px-1 py-0.5 rounded cursor-grab active:cursor-grabbing text-gray-600 hover:text-gray-400 hover:bg-white/5 transition-colors touch-none select-none"
title="Перетащить"
>
<GripVertical className="w-4 h-4" />
</div>
<button
onClick={onToggleCollapse}
className="p-1 rounded text-gray-600 hover:text-gray-300 hover:bg-white/5 transition-colors"
title={isCollapsed ? 'Развернуть' : 'Свернуть'}
type="button"
>
<ChevronRight className={`w-4 h-4 transition-transform duration-200 ${isCollapsed ? '' : 'rotate-90'}`} />
</button>
</div>
{/* Center: task count + deploy badge */}
<div className="flex items-center gap-2 flex-1 min-w-0">
{stats && stats.total > 0 && (
<div className="flex items-center gap-1 text-[10px] text-gray-500">
<span className="px-1.5 py-0.5 bg-white/5 rounded-full">
{stats.proposed > 0 && <span className="text-blue-400">{stats.proposed}</span>}
{stats.proposed > 0 && stats.approved > 0 && <span className="text-gray-600"> / </span>}
{stats.approved > 0 && <span className="text-emerald-400">{stats.approved}</span>}
{(stats.proposed > 0 || stats.approved > 0) && <span className="text-gray-600"> задач</span>}
</span>
</div>
)}
{deployLog && (
<div className={`flex items-center gap-1 text-[10px] ${deployStatusColor} min-w-0`} title={`${deployLog.branch} · ${deployLog.commit} · ${deployLog.actor}`}>
<GitBranch className="w-3 h-3 shrink-0" />
<span className="truncate">{deployRelative}</span>
</div>
)}
</div>
{/* Right: color picker + quick-create + arrows */}
<div className="flex items-center gap-0.5">
{onAddTask && (
<button
onClick={onAddTask}
className="p-1 rounded text-gray-600 hover:text-primary hover:bg-white/5 transition-colors"
title="Быстрая задача"
type="button"
>
<PlusCircle className="w-4 h-4" />
</button>
)}
<div className="relative">
<button
onClick={() => setShowColorPicker(v => !v)}
className="p-1 rounded text-gray-600 hover:text-gray-300 hover:bg-white/5 transition-colors"
title="Цвет карточки"
type="button"
>
<Palette className="w-4 h-4" style={cardColor ? { color: cardColor } : undefined} />
</button>
{showColorPicker && (
<div className="absolute right-0 top-7 z-50 bg-slate-800 border border-white/10 rounded-xl p-2 shadow-xl flex flex-wrap gap-1 w-[108px]">
{PRESET_COLORS.map(c => (
<button
key={c}
type="button"
onClick={() => { onColorChange?.(c); setShowColorPicker(false); }}
className={`w-7 h-7 rounded-full border-2 transition-transform hover:scale-110 ${cardColor === c ? 'border-white' : 'border-transparent'}`}
style={{ background: c }}
title={c}
/>
))}
<button
type="button"
onClick={() => { onColorChange?.(''); setShowColorPicker(false); }}
className="w-full text-[10px] text-gray-500 hover:text-gray-300 pt-1 transition-colors"
>
Сбросить
</button>
</div>
)}
</div>
<button
onClick={onMoveUp}
disabled={isFirst}
className="p-1 rounded text-gray-600 hover:text-gray-200 hover:bg-white/10 disabled:opacity-20 disabled:cursor-not-allowed transition-colors"
title="Переместить вверх"
type="button"
>
<ChevronUp className="w-4 h-4" />
</button>
<button
onClick={onMoveDown}
disabled={isLast}
className="p-1 rounded text-gray-600 hover:text-gray-200 hover:bg-white/10 disabled:opacity-20 disabled:cursor-not-allowed transition-colors"
title="Переместить вниз"
type="button"
>
<ChevronDown className="w-4 h-4" />
</button>
</div>
</div>
{/* Card content */}
{!isCollapsed && (
<div
className={isDragging ? 'shadow-2xl scale-[1.02] transition-transform' : ''}
style={cardColor ? { '--card-accent': cardColor, borderLeftColor: cardColor } as React.CSSProperties : undefined}
>
{children}
</div>
)}
{/* Collapsed placeholder */}
{isCollapsed && (
<div
className="bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 flex items-center gap-2 text-sm text-gray-500 cursor-pointer hover:bg-white/8 transition-colors"
onClick={onToggleCollapse}
style={cardColor ? { borderLeftColor: cardColor, borderLeftWidth: '4px' } : undefined}
>
<ChevronRight className="w-4 h-4" />
<span className="font-medium text-gray-400">{id}</span>
{stats && stats.total > 0 && (
<span className="ml-auto text-[11px] text-gray-600">{stats.total} задач</span>
)}
</div>
)}
</div>
);
}