import { HTMLAttributes, forwardRef, useState, useEffect, useRef } from 'react'; import { cn } from '@/lib/utils'; import { Card } from './Card'; import { Badge } from './Badge'; import { formatDateForDisplay } from '@/lib/date-utils'; interface TaskCardProps extends HTMLAttributes { // Variants variant?: 'compact' | 'detailed'; source?: 'manual' | 'jira' | 'tfs' | 'reminders'; // Content title: string; description?: string; tags?: string[]; priority?: 'low' | 'medium' | 'high' | 'urgent'; // Status & metadata status?: 'todo' | 'in_progress' | 'review' | 'done' | 'archived' | 'freeze' | 'backlog' | 'cancelled'; dueDate?: Date; completedAt?: Date; // Source-specific data jiraKey?: string; jiraProject?: string; jiraType?: string; tfsPullRequestId?: number; tfsProject?: string; tfsRepository?: string; // Task ID for todos count todosCount?: number; // Interactive isDragging?: boolean; isPending?: boolean; isEditing?: boolean; onEdit?: (e?: React.MouseEvent) => void; onDelete?: (e?: React.MouseEvent) => void; onTitleClick?: (e?: React.MouseEvent) => void; onTitleSave?: (title: string) => void; // Styling & behavior className?: string; fontSize?: 'small' | 'medium' | 'large'; availableTags?: Array<{ id: string; name: string; color: string }>; jiraConfig?: { baseUrl?: string }; tfsConfig?: { organizationUrl?: string }; } const TaskCard = forwardRef( ({ variant = 'detailed', source = 'manual', title, description, tags = [], priority = 'medium', status, dueDate, completedAt, jiraKey, jiraProject, jiraType, tfsPullRequestId, tfsProject, tfsRepository, todosCount, isDragging = false, isPending = false, onEdit, onDelete, onTitleClick, onTitleSave, className, fontSize = 'medium', availableTags = [], jiraConfig, tfsConfig, children, ...props }, ref) => { // État local pour l'édition du titre const [isEditingTitle, setIsEditingTitle] = useState(false); const [editTitle, setEditTitle] = useState(title); const timeoutRef = useRef(null); // Mettre à jour le titre local quand la prop change useEffect(() => { setEditTitle(title); }, [title]); // Nettoyer le timeout au démontage useEffect(() => { const currentTimeout = timeoutRef.current; return () => { if (currentTimeout) { clearTimeout(currentTimeout); } }; }, []); // Gestionnaires d'événements avec preventDefault/stopPropagation const handleEdit = (e?: React.MouseEvent) => { e?.preventDefault(); e?.stopPropagation(); onEdit?.(e); }; const handleDelete = (e?: React.MouseEvent) => { e?.preventDefault(); e?.stopPropagation(); onDelete?.(e); }; const handleTitleClick = (e?: React.MouseEvent) => { e?.preventDefault(); e?.stopPropagation(); if (!isDragging && !isPending) { setIsEditingTitle(true); } onTitleClick?.(e); }; const handleTitleSave = async () => { const trimmedTitle = editTitle.trim(); if (trimmedTitle && trimmedTitle !== title) { onTitleSave?.(trimmedTitle); } setIsEditingTitle(false); }; const handleTitleCancel = () => { setEditTitle(title); setIsEditingTitle(false); }; const handleTitleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); handleTitleSave(); } else if (e.key === 'Escape') { e.preventDefault(); handleTitleCancel(); } }; // Classes CSS pour les différentes tailles de police const getFontSizeClasses = () => { switch (fontSize) { case 'small': return { title: 'text-xs', description: 'text-xs', meta: 'text-xs', }; case 'large': return { title: 'text-base', description: 'text-sm', meta: 'text-sm', }; default: // medium return { title: 'text-sm', description: 'text-xs', meta: 'text-xs', }; } }; const fontClasses = getFontSizeClasses(); // Styles spéciaux pour les sources const getSourceStyles = () => { if (source === 'jira') { return { backgroundColor: 'var(--jira-card, #dbeafe)', borderLeft: '3px solid var(--jira-border, #3b82f6)', color: 'var(--jira-text, #1e40af)', }; } if (source === 'tfs') { return { backgroundColor: 'var(--tfs-card, #fed7aa)', borderLeft: '3px solid var(--tfs-border, #f59e0b)', color: 'var(--tfs-text, #92400e)', }; } return {}; }; // Couleurs de priorité const getPriorityColor = (priority: string) => { const colors = { low: '#10b981', // green medium: '#f59e0b', // amber high: '#ef4444', // red urgent: '#dc2626', // red-600 }; return colors[priority as keyof typeof colors] || colors.medium; }; // Extraire les emojis du titre const emojiRegex = /(?:[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}])(?:[\u{200D}][\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{FE0F}])*/gu; const titleEmojis = title.match(emojiRegex) || []; const titleWithoutEmojis = title.replace(emojiRegex, '').trim(); // Si pas d'emoji dans le titre, utiliser l'emoji du premier tag let displayEmojis: string[] = titleEmojis; if (displayEmojis.length === 0 && tags && tags.length > 0) { const firstTag = availableTags.find((tag) => tag.name === tags[0]); if (firstTag) { const tagEmojis = firstTag.name.match(emojiRegex); if (tagEmojis && tagEmojis.length > 0) { displayEmojis = [tagEmojis[0]]; // Prendre seulement le premier emoji du tag } } } const sourceStyles = getSourceStyles(); const priorityColor = getPriorityColor(priority); // Vue compacte if (variant === 'compact') { return (
{/* Emojis */} {displayEmojis.length > 0 && (
{displayEmojis.slice(0, 1).map((emoji, index) => ( {emoji} ))}
)} {/* Titre ou input d'édition */} {isEditingTitle ? ( setEditTitle(e.target.value)} onKeyDown={handleTitleKeyPress} onBlur={handleTitleSave} autoFocus className={`flex-1 bg-transparent border-none outline-none text-[var(--foreground)] font-mono ${fontClasses.title} font-medium leading-tight`} /> ) : (

{titleWithoutEmojis}

)} {/* Actions */}
{!isEditingTitle && onEdit && ( )} {!isEditingTitle && onDelete && ( )} {/* Indicateur de priorité */}
); } // Vue détaillée return (
0)) ? 'pb-2' : 'pb-0'}`} style={sourceStyles}> {/* Header */}
{/* Emojis */} {displayEmojis.length > 0 && (
{displayEmojis.slice(0, 2).map((emoji, index) => ( {emoji} ))}
)} {/* Titre ou input d'édition */} {isEditingTitle ? ( setEditTitle(e.target.value)} onKeyDown={handleTitleKeyPress} onBlur={handleTitleSave} autoFocus className="flex-1 bg-transparent border-none outline-none text-[var(--foreground)] font-mono text-sm font-medium leading-tight" /> ) : (

{titleWithoutEmojis}

)} {/* Actions */}
{!isEditingTitle && onEdit && ( )} {!isEditingTitle && onDelete && ( )} {/* Indicateur de priorité */}
{/* Description */} {description && (

{description}

)} {/* Tags avec couleurs personnalisées */} {tags.length > 0 && (
{tags.slice(0, 3).map((tag, index) => { const tagConfig = availableTags.find(t => t.name === tag); return ( {tag} ); })} {tags.length > 3 && ( +{tags.length - 3} )}
)} {/* Footer avec métadonnées */} {(dueDate || (source && source !== 'manual') || completedAt || (todosCount !== undefined && todosCount > 0)) && (
{/* Date d'échéance */} {dueDate ? ( {formatDateForDisplay(dueDate, 'DISPLAY_MEDIUM')} ) : (
)} {/* Badges source */}
{/* Jira */} {source === 'jira' && jiraKey && ( jiraConfig?.baseUrl ? ( {jiraKey} ) : ( {jiraKey} ) )} {/* TFS */} {source === 'tfs' && tfsPullRequestId && tfsProject && tfsRepository && ( tfsConfig?.organizationUrl ? ( PR-{tfsPullRequestId} ) : ( PR-{tfsPullRequestId} ) )} {/* Projets */} {jiraProject && ( {jiraProject} )} {tfsRepository && ( {tfsRepository} )} {/* Type Jira */} {jiraType && ( {jiraType} )} {/* Statut terminé avec date de résolution */} {completedAt && ( {formatDateForDisplay(completedAt, 'DISPLAY_SHORT')} )} {/* Nombre de todos reliés */} {todosCount !== undefined && todosCount > 0 && ( 📝 {todosCount} )}
)} {/* Contenu personnalisé */} {children}
); } ); TaskCard.displayName = 'TaskCard'; export { TaskCard };