import { useState, useEffect, useRef, useTransition } from 'react'; import { Task } from '@/lib/types'; import { formatDistanceToNow } from 'date-fns'; import { fr } from 'date-fns/locale'; import { Card } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { TagDisplay } from '@/components/ui/TagDisplay'; import { useTasksContext } from '@/contexts/TasksContext'; import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { useDraggable } from '@dnd-kit/core'; import { getPriorityConfig, getPriorityColorHex } from '@/lib/status-config'; import { updateTaskTitle, deleteTask } from '@/actions/tasks'; interface TaskCardProps { task: Task; onEdit?: (task: Task) => void; compactView?: boolean; } export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) { const [isEditingTitle, setIsEditingTitle] = useState(false); const [editTitle, setEditTitle] = useState(task.title); const [showTooltip, setShowTooltip] = useState(false); const [isPending, startTransition] = useTransition(); const timeoutRef = useRef(null); const { tags: availableTags, refreshTasks } = useTasksContext(); const { preferences } = useUserPreferences(); // Classes CSS pour les différentes tailles de police const getFontSizeClasses = () => { switch (preferences.viewPreferences.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(); // Helper pour construire l'URL Jira const getJiraTicketUrl = (jiraKey: string): string => { const baseUrl = preferences.jiraConfig.baseUrl; if (!baseUrl || !jiraKey) return ''; return `${baseUrl}/browse/${jiraKey}`; }; // Configuration du draggable const { attributes, listeners, setNodeRef, transform, isDragging, } = useDraggable({ id: task.id, }); // Mettre à jour le titre local quand la tâche change useEffect(() => { setEditTitle(task.title); }, [task.title]); // Nettoyer le timeout au démontage useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); const handleDelete = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (window.confirm('Êtes-vous sûr de vouloir supprimer cette tâche ?')) { startTransition(async () => { const result = await deleteTask(task.id); if (!result.success) { console.error('Error deleting task:', result.error); // TODO: Afficher une notification d'erreur } else { // Rafraîchir les données après suppression réussie await refreshTasks(); } }); } }; const handleEdit = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (onEdit) { onEdit(task); } }; const handleTitleClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!isDragging && !isPending) { setIsEditingTitle(true); setShowTooltip(false); } }; const handleTitleSave = async () => { const trimmedTitle = editTitle.trim(); if (trimmedTitle && trimmedTitle !== task.title) { startTransition(async () => { const result = await updateTaskTitle(task.id, trimmedTitle); if (!result.success) { console.error('Error updating task title:', result.error); // Remettre l'ancien titre en cas d'erreur setEditTitle(task.title); } else { // Mettre à jour optimistiquement le titre local // La Server Action a déjà mis à jour la DB, on synchronise juste l'affichage task.title = trimmedTitle; } }); } setIsEditingTitle(false); setShowTooltip(false); }; const handleTitleCancel = () => { setEditTitle(task.title); setIsEditingTitle(false); setShowTooltip(false); }; const handleTitleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); handleTitleSave(); } else if (e.key === 'Escape') { e.preventDefault(); handleTitleCancel(); } }; const handleMouseEnter = () => { if (!isEditingTitle) { timeoutRef.current = setTimeout(() => { setShowTooltip(true); }, 100); } }; const handleMouseLeave = () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } setShowTooltip(false); }; // Style de transformation pour le drag const style = transform ? { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, } : undefined; // Extraire les emojis du titre pour les afficher comme tags visuels 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 = task.title.match(emojiRegex) || []; const titleWithoutEmojis = task.title.replace(emojiRegex, '').trim(); // Composant titre avec tooltip const TitleWithTooltip = () => (

{titleWithoutEmojis}

{/* Tooltip */} {showTooltip && (
{titleWithoutEmojis}
)}
); // Si pas d'emoji dans le titre, utiliser l'emoji du premier tag let displayEmojis: string[] = titleEmojis; if (displayEmojis.length === 0 && task.tags && task.tags.length > 0) { const firstTag = availableTags.find(tag => tag.name === task.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 } } } // Styles spéciaux pour les tâches Jira const isJiraTask = task.source === 'jira'; const jiraStyles = isJiraTask ? { border: '1px solid rgba(0, 130, 201, 0.3)', borderLeft: '3px solid #0082C9', background: 'linear-gradient(135deg, rgba(0, 130, 201, 0.05) 0%, rgba(0, 130, 201, 0.02) 100%)' } : {}; // Vue compacte : seulement le titre if (compactView) { return (
{displayEmojis.length > 0 && (
{displayEmojis.slice(0, 1).map((emoji, index) => ( {emoji} ))}
)} {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`} /> ) : ( )}
{/* Boutons d'action compacts - masqués en mode édition */} {!isEditingTitle && onEdit && ( )} {!isEditingTitle && ( )} {/* Indicateur de priorité compact */}
); } // Vue détaillée : version complète return ( {/* Header tech avec titre et status */}
{displayEmojis.length > 0 && (
{displayEmojis.slice(0, 2).map((emoji, index) => ( {emoji} ))}
)} {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" /> ) : ( )}
{/* Bouton d'édition discret - masqué en mode édition */} {!isEditingTitle && onEdit && ( )} {/* Bouton de suppression discret - masqué en mode édition */} {!isEditingTitle && ( )} {/* Indicateur de priorité tech */}
{/* Description tech */} {task.description && (

{task.description}

)} {/* Tags avec couleurs */} {task.tags && task.tags.length > 0 && (
)} {/* Footer tech avec séparateur néon - seulement si des données à afficher */} {(task.dueDate || (task.source && task.source !== 'manual') || task.completedAt) && (
{task.dueDate ? ( {formatDistanceToNow(new Date(task.dueDate), { addSuffix: true, locale: fr })} ) : (
)}
{task.source !== 'manual' && task.source && ( task.source === 'jira' && task.jiraKey ? ( preferences.jiraConfig.baseUrl ? ( e.stopPropagation()} className="hover:scale-105 transition-transform" > {task.jiraKey} ) : ( {task.jiraKey} ) ) : ( {task.source} ) )} {task.jiraProject && ( {task.jiraProject} )} {task.jiraType && ( {task.jiraType} )} {task.completedAt && ( ✓ DONE )}
)} ); }